diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2c53bbe35..4cc2ad7dc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,6 +8,7 @@ on: - users tags: - "v*" + - "!*-fdroid" pull_request: jobs: @@ -52,15 +53,19 @@ jobs: - os: ubuntu-20.04 cache_path: ~/.cabal/store asset_name: simplex-chat-ubuntu-20_04-x86-64 + desktop_asset_name: simplex-desktop-ubuntu-20_04-x86_64.deb - os: ubuntu-22.04 cache_path: ~/.cabal/store asset_name: simplex-chat-ubuntu-22_04-x86-64 + desktop_asset_name: simplex-desktop-ubuntu-22_04-x86_64.deb - os: macos-latest cache_path: ~/.cabal/store asset_name: simplex-chat-macos-x86-64 + desktop_asset_name: simplex-desktop-macos-x86_64.dmg - os: windows-latest cache_path: C:/cabal asset_name: simplex-chat-windows-x86-64 + desktop_asset_name: simplex-desktop-windows-x86_64.msi steps: - name: Configure pagefile (Windows) if: matrix.os == 'windows-latest' @@ -99,6 +104,10 @@ jobs: echo " extra-lib-dirs: /usr/local/opt/openssl@1.1/lib" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local + - name: Install AppImage dependencies + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + run: sudo apt install -y desktop-file-utils + - name: Install pkg-config for Mac if: matrix.os == 'macos-latest' run: brew install pkg-config @@ -111,23 +120,136 @@ jobs: echo "package direct-sqlcipher" >> cabal.project.local echo " flags: +openssl" >> cabal.project.local - - name: Unix build - id: unix_build + - name: Unix build CLI + id: unix_cli_build if: matrix.os != 'windows-latest' shell: bash run: | cabal build --enable-tests - echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)" + path=$(cabal list-bin simplex-chat) + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Unix upload binary to release + - name: Unix upload CLI binary to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} - file: ${{ steps.unix_build.outputs.bin_path }} + file: ${{ steps.unix_cli_build.outputs.bin_path }} asset_name: ${{ matrix.asset_name }} tag: ${{ github.ref }} + - name: Unix update CLI binary hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.unix_cli_build.outputs.bin_hash }} + + - name: Setup Java + if: startsWith(github.ref, 'refs/tags/v') + uses: actions/setup-java@v3 + with: + distribution: 'corretto' + java-version: '17' + cache: 'gradle' + + - name: Linux build desktop + id: linux_desktop_build + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + shell: bash + run: | + scripts/desktop/build-lib-linux.sh + cd apps/multiplatform + ./gradlew packageDeb + path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb) + echo "package_path=$path" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Linux make AppImage + id: linux_appimage_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + shell: bash + run: | + scripts/desktop/make-appimage-linux.sh + path=$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage) + echo "appimage_path=$path" >> $GITHUB_OUTPUT + echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Mac build desktop + id: mac_desktop_build + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + shell: bash + env: + APPLE_SIMPLEX_SIGNING_KEYCHAIN: ${{ secrets.APPLE_SIMPLEX_SIGNING_KEYCHAIN }} + APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }} + APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }} + run: | + scripts/build-desktop-mac.sh + path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg) + echo "package_path=$path" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT + + - name: Linux upload desktop package to release + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.linux_desktop_build.outputs.package_path }} + asset_name: ${{ matrix.desktop_asset_name }} + tag: ${{ github.ref }} + + - name: Linux update desktop package hash + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.linux_desktop_build.outputs.package_hash }} + + - name: Linux upload AppImage to release + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.linux_appimage_build.outputs.appimage_path }} + asset_name: simplex-desktop-x86_64.AppImage + tag: ${{ github.ref }} + + - name: Linux update AppImage hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.linux_appimage_build.outputs.appimage_hash }} + + - name: Mac upload desktop package to release + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: ${{ steps.mac_desktop_build.outputs.package_path }} + asset_name: ${{ matrix.desktop_asset_name }} + tag: ${{ github.ref }} + + - name: Mac update desktop package hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.mac_desktop_build.outputs.package_hash }} + - name: Unix test if: matrix.os != 'windows-latest' timeout-minutes: 30 @@ -147,11 +269,12 @@ jobs: shell: cmd run: | cabal build --enable-tests - cabal list-bin simplex-chat > tmp_bin_path - set /p bin_path= < tmp_bin_path - echo ::set-output name=bin_path::%bin_path% + rm -rf dist-newstyle/src/direct-sq* + path=$(cabal list-bin simplex-chat | tail -n 1) + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Windows upload binary to release + - name: Windows upload CLI binary to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' uses: svenstaro/upload-release-action@v2 with: @@ -160,4 +283,14 @@ jobs: asset_name: ${{ matrix.asset_name }} tag: ${{ github.ref }} + - name: Windows update CLI binary hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.windows_build.outputs.bin_hash }} + # Windows / diff --git a/.gitignore b/.gitignore index 44fd61e359..d7106ec8fa 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ website/src/docs/ website/translations.json website/src/img/images/ website/src/images/ +website/src/js/lottie.min.js # Generated files website/package/generated* diff --git a/README.md b/README.md index d4103fd104..66465926dd 100644 --- a/README.md +++ b/README.md @@ -119,19 +119,22 @@ Join our translators to help SimpleX grow! |locale|language |contributor|[Android](https://play.google.com/store/apps/details?id=chat.simplex.app) and [iOS](https://apps.apple.com/us/app/simplex-chat/id1605771084)|[website](https://simplex.chat)|Github docs| |:----:|:-------:|:---------:|:---------:|:---------:|:---------:| |🇬🇧 en|English | |✓|✓|✓|✓| -|ar|العربية |[jermanuts](https://github.com/jermanuts)||[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)|| -|🇧🇬 bg|Български |-|[![android app](https://hosted.weblate.org/widgets/simplex-chat/bg/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/bg/)
-||| +|ar|العربية |[jermanuts](https://github.com/jermanuts)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/ar/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ar/)
-|[![website](https://hosted.weblate.org/widgets/simplex-chat/ar/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ar/)|| +|🇧🇬 bg|Български | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/bg/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/bg/)
[![ios app](https://hosted.weblate.org/widget/simplex-chat/ios/bg/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/bg/)||| |🇨🇿 cs|Čeština |[zen0bit](https://github.com/zen0bit)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/cs/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/cs/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/cs/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/cs/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/cs/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/cs/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/cs)| |🇩🇪 de|Deutsch |[mlanp](https://github.com/mlanp)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/de/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/de/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/de/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/de/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/de/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/de/)|| |🇪🇸 es|Español |[Mateyhv](https://github.com/Mateyhv)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/es/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/es/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/es/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/es/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/es/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/es/)|| +|🇫🇮 fi|Suomi | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/fi/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fi/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fi/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fi/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fi/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fi/)|| |🇫🇷 fr|Français |[ishi_sama](https://github.com/ishi-sama)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/fr/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/fr/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/fr/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/fr/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/fr/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/fr/)|[✓](https://github.com/simplex-chat/simplex-chat/tree/master/docs/lang/fr)| +|🇮🇱 he|עִברִית | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/he/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/he/)
-||| |🇮🇹 it|Italiano |[unbranched](https://github.com/unbranched)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/it/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/it/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/it/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/it/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/it/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/it/)|| -|🇯🇵 ja|Japanese ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)||| +|🇯🇵 ja|日本語 | |[![android app](https://hosted.weblate.org/widgets/simplex-chat/ja/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ja/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ja/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ja/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/ja/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/ja/)|| |🇳🇱 nl|Nederlands|[mika-nl](https://github.com/mika-nl)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/nl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/nl/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/nl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/nl/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/nl/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/nl/)|| |🇵🇱 pl|Polski |[BxOxSxS](https://github.com/BxOxSxS)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/pl/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pl/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/pl/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/pl/)||| |🇧🇷 pt-BR|Português||[![android app](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/pt_BR/)
-|[![website](https://hosted.weblate.org/widgets/simplex-chat/pt_BR/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/pt_BR/)|| |🇷🇺 ru|Русский ||[![android app](https://hosted.weblate.org/widgets/simplex-chat/ru/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/ru/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/ru/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/ru/)||| |🇹🇭 th|ภาษาไทย |[titapa-punpun](https://github.com/titapa-punpun)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/th/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/th/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/th/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/th/)||| +|🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)|| |🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)

[Float-hu](https://github.com/Float-hu)|[![android app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/zh_Hans/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/zh_Hans/)
 |

[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| Languages in progress: Arabic, Japanese, Korean, Portuguese and [others](https://hosted.weblate.org/projects/simplex-chat/#languages). We will be adding more languages as some of the already added are completed – please suggest new languages, review the [translation guide](./docs/TRANSLATIONS.md) and get in touch with us! @@ -227,24 +230,18 @@ You can use SimpleX with your own servers and still communicate with people usin ## News and updates -Recent updates: +Recent and important updates: -[July 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). +[Sep 25, 2023. SimpleX Chat v5.3 released: desktop app, local file encryption, improved groups and directory service](./blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md). + +[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](./blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). [May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). [Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). -[Mar 28, 2023. v4.6 released - with Android 8+ and ARMv7a support, hidden profiles, community moderation, improved audio/video calls and reduced battery usage](./blog/20230328-simplex-chat-v4-6-hidden-profiles.md). - [Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). -[Feb 4, 2023. v4.5 released - with multiple user profiles, message draft, transport isolation and Italian interface](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md). - -[Jan 3, 2023. v4.4 released - with disappearing messages, "live" messages, connection security verifications, GIFs and stickers and with French interface language](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md). - -[Dec 6, 2022. November reviews and v4.3 released - with instant voice messages, irreversible deletion of sent messages and improved server configuration](./blog/20221206-simplex-chat-v4.3-voice-messages.md). - [Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). [Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md). @@ -290,18 +287,20 @@ What is already implemented: 3. [Double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) end-to-end encryption in each conversation between two users (or group members). This is the same algorithm that is used in Signal and many other messaging apps; it provides OTR messaging with [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (each message is encrypted by its own ephemeral key) and [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (the keys are frequently re-negotiated as part of the message exchange). Two pairs of Curve448 keys are used for the initial [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), initiating party passes these keys via the connection link, accepting side - in the header of the confirmation message. 4. Additional layer of encryption using NaCL cryptobox for the messages delivered from the server to the recipient. This layer avoids having any ciphertext in common between sent and received traffic of the server inside TLS (and there are no identifiers in common as well). 5. Several levels of [content padding](./docs/GLOSSARY.md#message-padding) to frustrate message size attacks. -6. Starting from v2 of SMP protocol (the current version is v4) all message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. +6. All message metadata, including the time when the message was received by the server (rounded to a second) is sent to the recipients inside an encrypted envelope, so even if TLS is compromised it cannot be observed. 7. Only TLS 1.2/1.3 are allowed for client-server connections, limited to cryptographic algorithms: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 8. To protect against replay attacks SimpleX servers require [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) as session ID in each client command signed with per-queue ephemeral key. 9. To protect your IP address all SimpleX Chat clients support accessing messaging servers via Tor - see [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) for more details. 10. Local database encryption with passphrase - your contacts, groups and all sent and received messages are stored encrypted. If you used SimpleX Chat before v4.0 you need to enable the encryption via the app settings. 11. Transport isolation - different TCP connections and Tor circuits are used for traffic of different user profiles, optionally - for different contacts and group member connections. 12. Manual messaging queue rotations to move conversation to another SMP relay. +13. Sending end-to-end encrypted files using [XFTP protocol](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). +14. Local files encryption, except videos (to be added later). We plan to add: -1. Local files encryption. Currently the images and files you send and receive are stored in the app unencrypted, you can delete them via `Settings / Database passphrase & export`. This is currently in progress. -2. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party. +1. Senders' SMP relays and recipients' XFTP relays to reduce traffic and conceal IP addresses from the relays chosen, and potentially controlled, by another party. +2. Post-quantum resistant key exchange in double ratchet protocol. 3. Automatic message queue rotation and redundancy. Currently the queues created between two users are used until the queue is manually changed by the user or contact is deleted. We are planning to add automatic queue rotation to make these identifiers temporary and rotate based on some schedule TBC (e.g., every X messages, or every X hours/days). 4. Message "mixing" - adding latency to message delivery, to protect against traffic correlation by message time. 5. Reproducible builds – this is the limitation of the development stack, but we will be investing into solving this problem. Users can still build all applications and services from the source code. @@ -365,22 +364,26 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A - ✅ Message editing history - ✅ Reduced battery and traffic usage in large groups. - ✅ Message delivery confirmation (with sender opt-out per contact). -- 🏗 Desktop client. +- ✅ Desktop client. +- ✅ Encryption of local files stored in the app. +- 🏗 Using mobile profiles from the desktop app. +- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). +- Post-quantum resistant key exchange in double ratchet protocol. +- Large groups, communities and public channels. +- Privacy & security slider - a simple way to set all settings at once. +- Improve sending videos (including encryption of locally stored videos). +- Improve experience for the new users. - SMP queue redundancy and rotation (manual is supported). - Include optional message into connection request sent via contact address. -- Local app files encryption. - Improved navigation and search in the conversation (expand and scroll to quoted message, scroll to search results, etc.). -- Large groups, communities and public channels. - Feeds/broadcasts. - Ephemeral/disappearing/OTR conversations with the existing contacts. - Privately share your location. - Web widgets for custom interactivity in the chats. - Programmable chat automations / rules (automatic replies/forward/deletion/sending, reminders, etc.). -- Supporting the same profile on multiple devices. - Privacy-preserving identity server for optional DNS-based contact/group addresses to simplify connection and discovery, but not used to deliver messages: - keep all your contacts and groups even if you lose the domain. - the server doesn't have information about your contacts and groups. -- Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - High capacity multi-node SMP relays. ## Disclaimers diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index f2e67c4aa5..3dbcf47004 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -28,6 +28,17 @@ struct ContentView: View { @State private var showWhatsNew = false @State private var showChooseLAMode = false @State private var showSetPasscode = false + @State private var chatListActionSheet: ChatListActionSheet? = nil + + private enum ChatListActionSheet: Identifiable { + case connectViaUrl(action: ConnReqType, link: String) + + var id: String { + switch self { + case let .connectViaUrl(_, link): return "connectViaUrl \(link)" + } + } + } var body: some View { ZStack { @@ -80,6 +91,11 @@ struct ContentView: View { if case .onboardingComplete = step, chatModel.currentUser != nil { mainView() + .actionSheet(item: $chatListActionSheet) { sheet in + switch sheet { + case let .connectViaUrl(action, link): return connectViaUrlSheet(action, link) + } + } } else { OnboardingView(onboarding: step) } @@ -132,7 +148,9 @@ struct ContentView: View { } } prefShowLANotice = true + connectViaUrl() } + .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } .sheet(isPresented: $showWhatsNew) { WhatsNewView() } @@ -265,36 +283,40 @@ struct ContentView: View { secondaryButton: .cancel() ) } -} -func connectViaUrl() { - let m = ChatModel.shared - if let url = m.appOpenUrl { - m.appOpenUrl = nil - AlertManager.shared.showAlert(connectViaUrlAlert(url)) + func connectViaUrl() { + dismissAllSheets() { + let m = ChatModel.shared + if let url = m.appOpenUrl { + m.appOpenUrl = nil + var path = url.path + logger.debug("ContentView.connectViaUrl path: \(path)") + if (path == "/contact" || path == "/invitation") { + path.removeFirst() + let action: ConnReqType = path == "contact" ? .contact : .invitation + let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") + chatListActionSheet = .connectViaUrl(action: action, link: link) + } else { + AlertManager.shared.showAlert(Alert(title: Text("Error: URL is invalid"))) + } + } + } } -} -func connectViaUrlAlert(_ url: URL) -> Alert { - var path = url.path - logger.debug("ChatListView.connectViaUrlAlert path: \(path)") - if (path == "/contact" || path == "/invitation") { - path.removeFirst() - let action: ConnReqType = path == "contact" ? .contact : .invitation - let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") + private func connectViaUrlSheet(_ action: ConnReqType, _ link: String) -> ActionSheet { let title: LocalizedStringKey - if case .contact = action { title = "Connect via contact link?" } - else { title = "Connect via one-time link?" } - return Alert( + switch action { + case .contact: title = "Connect via contact link" + case .invitation: title = "Connect via one-time link" + } + return ActionSheet( title: Text(title), - message: Text("Your profile will be sent to the contact that you received this link from"), - primaryButton: .default(Text("Connect")) { - connectViaLink(link) - }, - secondaryButton: .cancel() + buttons: [ + .default(Text("Use current profile")) { connectViaLink(link, incognito: false) }, + .default(Text("Use new incognito profile")) { connectViaLink(link, incognito: true) }, + .cancel() + ] ) - } else { - return Alert(title: Text("Error: URL is invalid")) } } diff --git a/apps/ios/Shared/Model/AudioRecPlay.swift b/apps/ios/Shared/Model/AudioRecPlay.swift index 698799457e..973d79ab3c 100644 --- a/apps/ios/Shared/Model/AudioRecPlay.swift +++ b/apps/ios/Shared/Model/AudioRecPlay.swift @@ -103,9 +103,15 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { self.onFinishPlayback = onFinishPlayback } - func start(fileName: String, at: TimeInterval?) { - let url = getAppFilePath(fileName) - audioPlayer = try? AVAudioPlayer(contentsOf: url) + func start(fileSource: CryptoFile, at: TimeInterval?) { + let url = getAppFilePath(fileSource.filePath) + if let cfArgs = fileSource.cryptoArgs { + if let data = try? readCryptoFile(path: url.path, cryptoArgs: cfArgs) { + audioPlayer = try? AVAudioPlayer(data: data) + } + } else { + audioPlayer = try? AVAudioPlayer(contentsOf: url) + } audioPlayer?.delegate = self audioPlayer?.prepareToPlay() if let at = at { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 3179b4f862..a1ec2f8f53 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -11,6 +11,38 @@ import Combine import SwiftUI import SimpleXChat +actor TerminalItems { + private var terminalItems: [TerminalItem] = [] + + static let shared = TerminalItems() + + func items() -> [TerminalItem] { + terminalItems + } + + func add(_ item: TerminalItem) async { + addTermItem(&terminalItems, item) + let m = ChatModel.shared + if m.showingTerminal { + await MainActor.run { + addTermItem(&m.terminalItems, item) + } + } + } + + func addCommand(_ start: Date, _ cmd: ChatCommand, _ resp: ChatResponse) async { + await add(.cmd(start, cmd)) + await add(.resp(.now, resp)) + } +} + +private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) { + if items.count >= 200 { + items.removeFirst() + } + items.append(item) +} + final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var setDeliveryReceipts = false @@ -33,6 +65,7 @@ final class ChatModel: ObservableObject { @Published var chatToTop: String? @Published var groupMembers: [GroupMember] = [] // items in the terminal view + @Published var showingTerminal = false @Published var terminalItems: [TerminalItem] = [] @Published var userAddress: UserContactLink? @Published var chatItemTTL: ChatItemTTL = .none @@ -43,9 +76,8 @@ final class ChatModel: ObservableObject { @Published var tokenStatus: NtfTknStatus? @Published var notificationMode = NotificationsMode.off @Published var notificationPreview: NotificationPreviewMode = ntfPreviewModeGroupDefault.get() - @Published var incognito: Bool = incognitoGroupDefault.get() // pending notification actions - @Published var ntfContactRequest: ChatId? + @Published var ntfContactRequest: NTFContactRequest? @Published var ntfCallInvitationAction: (ChatId, NtfCallAction)? // current WebRTC call @Published var callInvitations: Dictionary = [:] @@ -463,18 +495,18 @@ final class ChatModel: ObservableObject { } } - func increaseUnreadCounter(user: User) { + func increaseUnreadCounter(user: any UserLike) { changeUnreadCounter(user: user, by: 1) NtfManager.shared.incNtfBadgeCount() } - func decreaseUnreadCounter(user: User, by: Int = 1) { + func decreaseUnreadCounter(user: any UserLike, by: Int = 1) { changeUnreadCounter(user: user, by: -by) NtfManager.shared.decNtfBadgeCount(by: by) } - private func changeUnreadCounter(user: User, by: Int) { - if let i = users.firstIndex(where: { $0.user.id == user.id }) { + private func changeUnreadCounter(user: any UserLike, by: Int) { + if let i = users.firstIndex(where: { $0.user.userId == user.userId }) { users[i].unreadCount += by } } @@ -484,14 +516,27 @@ final class ChatModel: ObservableObject { users.filter { !$0.user.activeUser }.reduce(0, { unread, next -> Int in unread + next.unreadCount }) } - func getPrevChatItem(_ ci: ChatItem) -> ChatItem? { - if let i = getChatItemIndex(ci), i < reversedChatItems.count - 1 { - return reversedChatItems[i + 1] + func getConnectedMemberNames(_ ci: ChatItem) -> [String] { + guard var i = getChatItemIndex(ci) else { return [] } + var ns: [String] = [] + while i < reversedChatItems.count, let m = reversedChatItems[i].memberConnected { + ns.append(m.displayName) + i += 1 + } + return ns + } + + func getChatItemNeighbors(_ ci: ChatItem) -> (ChatItem?, ChatItem?) { + if let i = getChatItemIndex(ci) { + return ( + i + 1 < reversedChatItems.count ? reversedChatItems[i + 1] : nil, + i - 1 >= 0 ? reversedChatItems[i - 1] : nil + ) } else { - return nil + return (nil, nil) } } - + func popChat(_ id: String) { if let i = getChatIndex(id) { popChat_(i) @@ -580,13 +625,11 @@ final class ChatModel: ObservableObject { func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { networkStatuses[contact.activeConn.agentConnId] ?? .unknown } +} - func addTerminalItem(_ item: TerminalItem) { - if terminalItems.count >= 500 { - terminalItems.remove(at: 0) - } - terminalItems.append(item) - } +struct NTFContactRequest { + var incognito: Bool + var chatId: String } struct UnreadChatItemCounts { diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/Shared/Model/ImageUtils.swift index 4987f5a6f7..90070e74d3 100644 --- a/apps/ios/Shared/Model/ImageUtils.swift +++ b/apps/ios/Shared/Model/ImageUtils.swift @@ -11,42 +11,43 @@ import SimpleXChat import SwiftUI import AVKit -func getLoadedFilePath(_ file: CIFile?) -> String? { - if let fileName = getLoadedFileName(file) { - return getAppFilePath(fileName).path - } - return nil -} - -func getLoadedFileName(_ file: CIFile?) -> String? { - if let file = file, - file.loaded, - let fileName = file.filePath { - return fileName +func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? { + if let file = file, file.loaded { + return file.fileSource } return nil } func getLoadedImage(_ file: CIFile?) -> UIImage? { - let loadedFilePath = getLoadedFilePath(file) - if let loadedFilePath = loadedFilePath, let fileName = file?.filePath { - let filePath = getAppFilePath(fileName) + if let fileSource = getLoadedFileSource(file) { + let filePath = getAppFilePath(fileSource.filePath) do { - let data = try Data(contentsOf: filePath) + let data = try getFileData(filePath, fileSource.cryptoArgs) let img = UIImage(data: data) - try img?.setGifFromData(data, levelOfIntegrity: 1.0) - return img + do { + try img?.setGifFromData(data, levelOfIntegrity: 1.0) + return img + } catch { + return UIImage(data: data) + } } catch { - return UIImage(contentsOfFile: loadedFilePath) + return nil } } return nil } +func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { + if let cfArgs = cfArgs { + return try readCryptoFile(path: path.path, cryptoArgs: cfArgs) + } else { + return try Data(contentsOf: path) + } +} + func getLoadedVideo(_ file: CIFile?) -> URL? { - let loadedFilePath = getLoadedFilePath(file) - if loadedFilePath != nil, let fileName = file?.filePath { - let filePath = getAppFilePath(fileName) + if let fileSource = getLoadedFileSource(file) { + let filePath = getAppFilePath(fileSource.filePath) if FileManager.default.fileExists(atPath: filePath.path) { return filePath } @@ -54,18 +55,18 @@ func getLoadedVideo(_ file: CIFile?) -> URL? { return nil } -func saveAnimImage(_ image: UIImage) -> String? { +func saveAnimImage(_ image: UIImage) -> CryptoFile? { let fileName = generateNewFileName("IMG", "gif") guard let imageData = image.imageData else { return nil } - return saveFile(imageData, fileName) + return saveFile(imageData, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get()) } -func saveImage(_ uiImage: UIImage) -> String? { +func saveImage(_ uiImage: UIImage) -> CryptoFile? { let hasAlpha = imageHasAlpha(uiImage) let ext = hasAlpha ? "png" : "jpg" if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) { let fileName = generateNewFileName("IMG", ext) - return saveFile(imageDataResized, fileName) + return saveFile(imageDataResized, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get()) } return nil } @@ -157,13 +158,19 @@ func imageHasAlpha(_ img: UIImage) -> Bool { return false } -func saveFileFromURL(_ url: URL) -> String? { - let savedFile: String? +func saveFileFromURL(_ url: URL, encrypted: Bool) -> CryptoFile? { + let savedFile: CryptoFile? if url.startAccessingSecurityScopedResource() { do { - let fileData = try Data(contentsOf: url) let fileName = uniqueCombine(url.lastPathComponent) - savedFile = saveFile(fileData, fileName) + let toPath = getAppFilePath(fileName).path + if encrypted { + let cfArgs = try encryptCryptoFile(fromPath: url.path, toPath: toPath) + savedFile = CryptoFile(filePath: fileName, cryptoArgs: cfArgs) + } else { + try FileManager.default.copyItem(atPath: url.path, toPath: toPath) + savedFile = CryptoFile.plain(fileName) + } } catch { logger.error("FileUtils.saveFileFromURL error: \(error.localizedDescription)") savedFile = nil @@ -176,18 +183,16 @@ func saveFileFromURL(_ url: URL) -> String? { return savedFile } -func saveFileFromURLWithoutLoad(_ url: URL) -> String? { - let savedFile: String? +func moveTempFileFromURL(_ url: URL) -> CryptoFile? { do { let fileName = uniqueCombine(url.lastPathComponent) try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName)) ChatModel.shared.filesToDelete.remove(url) - savedFile = fileName + return CryptoFile.plain(fileName) } catch { - logger.error("FileUtils.saveFileFromURLWithoutLoad error: \(error.localizedDescription)") - savedFile = nil + logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)") + return nil } - return savedFile } func generateNewFileName(_ prefix: String, _ ext: String) -> String { @@ -288,4 +293,4 @@ extension UIImage { } return self } -} \ No newline at end of file +} diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index ad41703c91..f1fdcc018e 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -12,6 +12,7 @@ import UIKit import SimpleXChat let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT" +let ntfActionAcceptContactIncognito = "NTF_ACT_ACCEPT_CONTACT_INCOGNITO" let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL" let ntfActionRejectCall = "NTF_ACT_REJECT_CALL" @@ -41,12 +42,13 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { userId != chatModel.currentUser?.userId { changeActiveUser(userId, viewPwd: nil) } - if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact, + if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito), let chatId = content.userInfo["chatId"] as? String { + let incognito = action == ntfActionAcceptContactIncognito if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo { - Task { await acceptContactRequest(contactRequest) } + Task { await acceptContactRequest(incognito: incognito, contactRequest: contactRequest) } } else { - chatModel.ntfContactRequest = chatId + chatModel.ntfContactRequest = NTFContactRequest(incognito: incognito, chatId: chatId) } } else if let (chatId, ntfAction) = ntfCallAction(content, action) { if let invitation = chatModel.callInvitations.removeValue(forKey: chatId) { @@ -134,11 +136,17 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { UNUserNotificationCenter.current().setNotificationCategories([ UNNotificationCategory( identifier: ntfCategoryContactRequest, - actions: [UNNotificationAction( - identifier: ntfActionAcceptContact, - title: NSLocalizedString("Accept", comment: "accept contact request via notification"), - options: .foreground - )], + actions: [ + UNNotificationAction( + identifier: ntfActionAcceptContact, + title: NSLocalizedString("Accept", comment: "accept contact request via notification"), + options: .foreground + ), UNNotificationAction( + identifier: ntfActionAcceptContactIncognito, + title: NSLocalizedString("Accept incognito", comment: "accept contact request via notification"), + options: .foreground + ) + ], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: NSLocalizedString("New contact request", comment: "notification") ), @@ -203,17 +211,17 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { center.delegate = self } - func notifyContactRequest(_ user: User, _ contactRequest: UserContactRequest) { + func notifyContactRequest(_ user: any UserLike, _ contactRequest: UserContactRequest) { logger.debug("NtfManager.notifyContactRequest") addNotification(createContactRequestNtf(user, contactRequest)) } - func notifyContactConnected(_ user: User, _ contact: Contact) { + func notifyContactConnected(_ user: any UserLike, _ contact: Contact) { logger.debug("NtfManager.notifyContactConnected") addNotification(createContactConnectedNtf(user, contact)) } - func notifyMessageReceived(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) { + func notifyMessageReceived(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) { logger.debug("NtfManager.notifyMessageReceived") if cInfo.ntfsEnabled { addNotification(createMessageReceivedNtf(user, cInfo, cItem)) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 74395302a8..aef8711f34 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -15,6 +15,12 @@ import SimpleXChat private var chatController: chat_ctrl? +// currentChatVersion in core +public let CURRENT_CHAT_VERSION: Int = 2 + +// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) +public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION) + enum TerminalItem: Identifiable { case cmd(Date, ChatCommand) case resp(Date, ChatResponse) @@ -94,9 +100,8 @@ func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = if case let .response(_, json) = resp { logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") } - DispatchQueue.main.async { - ChatModel.shared.addTerminalItem(.cmd(start, cmd.obfuscated)) - ChatModel.shared.addTerminalItem(.resp(.now, resp)) + Task { + await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) } return resp } @@ -171,6 +176,12 @@ func apiSetUserContactReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgR throw r } +func apiSetUserGroupReceipts(_ userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) async throws { + let r = await chatSendCmd(.apiSetUserGroupReceipts(userId: userId, userMsgReceiptSettings: userMsgReceiptSettings)) + if case .cmdOk = r { return } + throw r +} + func apiHideUser(_ userId: Int64, viewPwd: String) async throws -> User { try await setUserPrivacy_(.apiHideUser(userId: userId, viewPwd: viewPwd)) } @@ -246,12 +257,6 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { throw r } -func apiSetIncognito(incognito: Bool) throws { - let r = chatSendCmdSync(.setIncognito(incognito: incognito)) - if case .cmdOk = r { return } - throw r -} - func apiExportArchive(config: ArchiveConfig) async throws { try await sendCommandOkResp(.apiExportArchive(config: config)) } @@ -316,17 +321,23 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws - throw r } -func apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? { +func apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool = false, ttl: Int? = nil) async -> ChatItem? { let chatModel = ChatModel.shared let cmd: ChatCommand = .apiSendMessage(type: type, id: id, file: file, quotedItemId: quotedItemId, msg: msg, live: live, ttl: ttl) let r: ChatResponse if type == .direct { - var cItem: ChatItem! - let endTask = beginBGTask({ if cItem != nil { chatModel.messageDelivery.removeValue(forKey: cItem.id) } }) + var cItem: ChatItem? = nil + let endTask = beginBGTask({ + if let cItem = cItem { + DispatchQueue.main.async { + chatModel.messageDelivery.removeValue(forKey: cItem.id) + } + } + }) r = await chatSendCmd(cmd, bgTask: false) if case let .newChatItem(_, aChatItem) = r { cItem = aChatItem.chatItem - chatModel.messageDelivery[cItem.id] = endTask + chatModel.messageDelivery[aChatItem.chatItem.id] = endTask return cItem } if let networkErrorAlert = networkErrorAlert(r) { @@ -558,19 +569,25 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo return nil } -func apiAddContact() async -> String? { +func apiAddContact(incognito: Bool) async -> (String, PendingContactConnection)? { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiAddContact: no current user") return nil } - let r = await chatSendCmd(.apiAddContact(userId: userId), bgTask: false) - if case let .invitation(_, connReqInvitation) = r { return connReqInvitation } + let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) + if case let .invitation(_, connReqInvitation, connection) = r { return (connReqInvitation, connection) } AlertManager.shared.showAlert(connectionErrorAlert(r)) return nil } -func apiConnect(connReq: String) async -> ConnReqType? { - let (connReqType, alert) = await apiConnect_(connReq: connReq) +func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> PendingContactConnection? { + let r = await chatSendCmd(.apiSetConnectionIncognito(connId: connId, incognito: incognito)) + if case let .connectionIncognitoUpdated(_, toConnection) = r { return toConnection } + throw r +} + +func apiConnect(incognito: Bool, connReq: String) async -> ConnReqType? { + let (connReqType, alert) = await apiConnect_(incognito: incognito, connReq: connReq) if let alert = alert { AlertManager.shared.showAlert(alert) return nil @@ -579,12 +596,12 @@ func apiConnect(connReq: String) async -> ConnReqType? { } } -func apiConnect_(connReq: String) async -> (ConnReqType?, Alert?) { +func apiConnect_(incognito: Bool, connReq: String) async -> (ConnReqType?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnect: no current user") return (nil, nil) } - let r = await chatSendCmd(.apiConnect(userId: userId, connReq: connReq)) + let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq)) switch r { case .sentConfirmation: return (.invitation, nil) case .sentInvitation: return (.contact, nil) @@ -680,12 +697,12 @@ func apiListContacts() throws -> [Contact] { throw r } -func apiUpdateProfile(profile: Profile) async throws -> Profile? { +func apiUpdateProfile(profile: Profile) async throws -> (Profile, [Contact])? { let userId = try currentUserId("apiUpdateProfile") let r = await chatSendCmd(.apiUpdateProfile(userId: userId, profile: profile)) switch r { case .userProfileNoChange: return nil - case let .userProfileUpdated(_, _, toProfile): return toProfile + case let .userProfileUpdated(_, _, toProfile, updateSummary): return (toProfile, updateSummary.changedContacts) default: throw r } } @@ -695,7 +712,7 @@ func apiSetProfileAddress(on: Bool) async throws -> User? { let r = await chatSendCmd(.apiSetProfileAddress(userId: userId, on: on)) switch r { case .userProfileNoChange: return nil - case let .userProfileUpdated(user, _, _): return user + case let .userProfileUpdated(user, _, _, _): return user default: throw r } } @@ -760,8 +777,8 @@ func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContac } } -func apiAcceptContactRequest(contactReqId: Int64) async -> Contact? { - let r = await chatSendCmd(.apiAcceptContact(contactReqId: contactReqId)) +func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Contact? { + let r = await chatSendCmd(.apiAcceptContact(incognito: incognito, contactReqId: contactReqId)) let am = AlertManager.shared if case let .acceptingContactRequest(_, contact) = r { return contact } @@ -796,29 +813,35 @@ func apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) async throws { try await sendCommandOkResp(.apiChatUnread(type: type, id: id, unreadChat: unreadChat)) } -func receiveFile(user: User, fileId: Int64) async { - if let chatItem = await apiReceiveFile(fileId: fileId) { - DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) } +func receiveFile(user: any UserLike, fileId: Int64, encrypted: Bool, auto: Bool = false) async { + if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: encrypted, auto: auto) { + await chatItemSimpleUpdate(user, chatItem) } } -func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? { - let r = await chatSendCmd(.receiveFile(fileId: fileId, inline: inline)) +func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { + let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) let am = AlertManager.shared if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case .rcvFileAcceptedSndCancelled = r { - am.showAlertMsg( - title: "Cannot receive file", - message: "Sender cancelled file transfer." - ) + logger.debug("apiReceiveFile error: sender cancelled file transfer") + if !auto { + am.showAlertMsg( + title: "Cannot receive file", + message: "Sender cancelled file transfer." + ) + } } else if let networkErrorAlert = networkErrorAlert(r) { + logger.error("apiReceiveFile network error: \(String(describing: r))") am.showAlert(networkErrorAlert) } else { - logger.error("apiReceiveFile error: \(String(describing: r))") - switch r { - case .chatCmdError(_, .error(.fileAlreadyReceiving)): + switch chatError(r) { + case .fileCancelled: + logger.debug("apiReceiveFile ignoring fileCancelled error") + case .fileAlreadyReceiving: logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") default: + logger.error("apiReceiveFile error: \(String(describing: r))") am.showAlertMsg( title: "Error receiving file", message: "Error: \(String(describing: r))" @@ -830,7 +853,7 @@ func apiReceiveFile(fileId: Int64, inline: Bool? = nil) async -> AChatItem? { func cancelFile(user: User, fileId: Int64) async { if let chatItem = await apiCancelFile(fileId: fileId) { - DispatchQueue.main.async { chatItemSimpleUpdate(user, chatItem) } + await chatItemSimpleUpdate(user, chatItem) cleanupFile(chatItem) } } @@ -863,8 +886,8 @@ func networkErrorAlert(_ r: ChatResponse) -> Alert? { } } -func acceptContactRequest(_ contactRequest: UserContactRequest) async { - if let contact = await apiAcceptContactRequest(contactReqId: contactRequest.apiId) { +func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async { + if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) { let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) } } @@ -1073,6 +1096,18 @@ func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? { } } +func apiCreateMemberContact(_ groupId: Int64, _ groupMemberId: Int64) async throws -> Contact { + let r = await chatSendCmd(.apiCreateMemberContact(groupId: groupId, groupMemberId: groupMemberId)) + if case let .newMemberContact(_, contact, _, _) = r { return contact } + throw r +} + +func apiSendMemberContactInvitation(_ contactId: Int64, _ msg: MsgContent) async throws -> Contact { + let r = await chatSendCmd(.apiSendMemberContactInvitation(contactId: contactId, msg: msg), bgDelay: msgDelay) + if case let .newMemberContactSentInv(_, contact, _, _) = r { return contact } + throw r +} + func apiGetVersion() throws -> CoreVersionInfo { let r = chatSendCmdSync(.showVersion) if case let .versionInfo(info, _, _) = r { return info } @@ -1098,7 +1133,6 @@ func initializeChat(start: Bool, dbKey: String? = nil, refreshInvitations: Bool try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) try setXFTPConfig(getXFTPCfg()) - try apiSetIncognito(incognito: incognitoGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() if m.currentUser == nil { @@ -1233,38 +1267,50 @@ class ChatReceiver { } func processReceivedMsg(_ res: ChatResponse) async { + Task { + await TerminalItems.shared.add(.resp(.now, res)) + } let m = ChatModel.shared - await MainActor.run { - m.addTerminalItem(.resp(.now, res)) - logger.debug("processReceivedMsg: \(res.responseType)") - switch res { - case let .newContactConnection(user, connection): - if active(user) { + logger.debug("processReceivedMsg: \(res.responseType)") + switch res { + case let .newContactConnection(user, connection): + if active(user) { + await MainActor.run { m.updateContactConnection(connection) } - case let .contactConnectionDeleted(user, connection): - if active(user) { + } + case let .contactConnectionDeleted(user, connection): + if active(user) { + await MainActor.run { m.removeChat(connection.id) } - case let .contactConnected(user, contact, _): - if active(user) && contact.directOrUsed { + } + case let .contactConnected(user, contact, _): + if active(user) && contact.directOrUsed { + await MainActor.run { m.updateContact(contact) m.dismissConnReqView(contact.activeConn.id) m.removeChat(contact.activeConn.id) } - if contact.directOrUsed { - NtfManager.shared.notifyContactConnected(user, contact) - } + } + if contact.directOrUsed { + NtfManager.shared.notifyContactConnected(user, contact) + } + await MainActor.run { m.setContactNetworkStatus(contact, .connected) - case let .contactConnecting(user, contact): - if active(user) && contact.directOrUsed { + } + case let .contactConnecting(user, contact): + if active(user) && contact.directOrUsed { + await MainActor.run { m.updateContact(contact) m.dismissConnReqView(contact.activeConn.id) m.removeChat(contact.activeConn.id) } - case let .receivedContactRequest(user, contactRequest): - if active(user) { - let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) + } + case let .receivedContactRequest(user, contactRequest): + if active(user) { + let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) + await MainActor.run { if m.hasChat(contactRequest.id) { m.updateChatInfo(cInfo) } else { @@ -1274,255 +1320,318 @@ func processReceivedMsg(_ res: ChatResponse) async { )) } } - NtfManager.shared.notifyContactRequest(user, contactRequest) - case let .contactUpdated(user, toContact): - if active(user) && m.hasChat(toContact.id) { + } + NtfManager.shared.notifyContactRequest(user, contactRequest) + case let .contactUpdated(user, toContact): + if active(user) && m.hasChat(toContact.id) { + await MainActor.run { let cInfo = ChatInfo.direct(contact: toContact) m.updateChatInfo(cInfo) } - case let .contactsMerged(user, intoContact, mergedContact): - if active(user) && m.hasChat(mergedContact.id) { + } + case let .contactsMerged(user, intoContact, mergedContact): + if active(user) && m.hasChat(mergedContact.id) { + await MainActor.run { if m.chatId == mergedContact.id { m.chatId = intoContact.id } m.removeChat(mergedContact.id) } - case let .contactsSubscribed(_, contactRefs): - updateContactsStatus(contactRefs, status: .connected) - case let .contactsDisconnected(_, contactRefs): - updateContactsStatus(contactRefs, status: .disconnected) - case let .contactSubError(user, contact, chatError): + } + case let .contactsSubscribed(_, contactRefs): + await updateContactsStatus(contactRefs, status: .connected) + case let .contactsDisconnected(_, contactRefs): + await updateContactsStatus(contactRefs, status: .disconnected) + case let .contactSubError(user, contact, chatError): + await MainActor.run { if active(user) { m.updateContact(contact) } processContactSubError(contact, chatError) - case let .contactSubSummary(user, contactSubscriptions): + } + case let .contactSubSummary(_, contactSubscriptions): + await MainActor.run { for sub in contactSubscriptions { - if active(user) { - m.updateContact(sub.contact) - } +// no need to update contact here, and it is slow +// if active(user) { +// m.updateContact(sub.contact) +// } if let err = sub.contactError { processContactSubError(sub.contact, err) } else { m.setContactNetworkStatus(sub.contact, .connected) } } - case let .newChatItem(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem + } + case let .newChatItem(user, aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + await MainActor.run { if active(user) { m.addChatItem(cInfo, cItem) } else if cItem.isRcvNew && cInfo.ntfsEnabled { m.increaseUnreadCounter(user: user) } - if let file = cItem.autoReceiveFile() { - Task { - await receiveFile(user: user, fileId: file.fileId) - } + } + if let file = cItem.autoReceiveFile() { + Task { + await receiveFile(user: user, fileId: file.fileId, encrypted: cItem.encryptLocalFile, auto: true) } - if cItem.showNotification { + } + if cItem.showNotification { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + } + case let .chatItemStatusUpdated(user, aChatItem): + let cInfo = aChatItem.chatInfo + let cItem = aChatItem.chatItem + if !cItem.isDeletedContent { + let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true + if added && cItem.showNotification { NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) } - case let .chatItemStatusUpdated(user, aChatItem): - let cInfo = aChatItem.chatInfo - let cItem = aChatItem.chatItem - if !cItem.isDeletedContent { - let added = active(user) ? m.upsertChatItem(cInfo, cItem) : true - if added && cItem.showNotification { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) - } + } + if let endTask = m.messageDelivery[cItem.id] { + switch cItem.meta.itemStatus { + case .sndSent: endTask() + case .sndErrorAuth: endTask() + case .sndError: endTask() + default: () } - if let endTask = m.messageDelivery[cItem.id] { - switch cItem.meta.itemStatus { - case .sndSent: endTask() - case .sndErrorAuth: endTask() - case .sndError: endTask() - default: () - } - } - case let .chatItemUpdated(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - case let .chatItemReaction(user, _, r): - if active(user) { + } + case let .chatItemUpdated(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + case let .chatItemReaction(user, _, r): + if active(user) { + await MainActor.run { m.updateChatItem(r.chatInfo, r.chatReaction.chatItem) } - case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): - if !active(user) { - if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { + } + case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): + if !active(user) { + if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { + await MainActor.run { m.decreaseUnreadCounter(user: user) } - return } + return + } + await MainActor.run { if let toChatItem = toChatItem { _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) } else { m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem) } - case let .receivedGroupInvitation(user, groupInfo, _, _): - if active(user) { + } + case let .receivedGroupInvitation(user, groupInfo, _, _): + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) // update so that repeat group invitations are not duplicated // NtfManager.shared.notifyContactRequest(contactRequest) // TODO notifyGroupInvitation? } - case let .userAcceptedGroupSent(user, groupInfo, hostContact): - if !active(user) { return } + } + case let .userAcceptedGroupSent(user, groupInfo, hostContact): + if !active(user) { return } + await MainActor.run { m.updateGroup(groupInfo) if let hostContact = hostContact { m.dismissConnReqView(hostContact.activeConn.id) m.removeChat(hostContact.activeConn.id) } - case let .joinedGroupMemberConnecting(user, groupInfo, _, member): - if active(user) { + } + case let .joinedGroupMemberConnecting(user, groupInfo, _, member): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - case let .deletedMemberUser(user, groupInfo, _): // TODO update user member - if active(user) { + } + case let .deletedMemberUser(user, groupInfo, _): // TODO update user member + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .deletedMember(user, groupInfo, _, deletedMember): - if active(user) { + } + case let .deletedMember(user, groupInfo, _, deletedMember): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, deletedMember) } - case let .leftMember(user, groupInfo, member): - if active(user) { + } + case let .leftMember(user, groupInfo, member): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - case let .groupDeleted(user, groupInfo, _): // TODO update user member - if active(user) { + } + case let .groupDeleted(user, groupInfo, _): // TODO update user member + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .userJoinedGroup(user, groupInfo): - if active(user) { + } + case let .userJoinedGroup(user, groupInfo): + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .joinedGroupMember(user, groupInfo, member): - if active(user) { + } + case let .joinedGroupMember(user, groupInfo, member): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - case let .connectedToGroupMember(user, groupInfo, member, memberContact): - if active(user) { + } + case let .connectedToGroupMember(user, groupInfo, member, memberContact): + if active(user) { + await MainActor.run { _ = m.upsertGroupMember(groupInfo, member) } - if let contact = memberContact { + } + if let contact = memberContact { + await MainActor.run { m.setContactNetworkStatus(contact, .connected) } - case let .groupUpdated(user, toGroup): - if active(user) { + } + case let .groupUpdated(user, toGroup): + if active(user) { + await MainActor.run { m.updateGroup(toGroup) } - case let .memberRole(user, groupInfo, _, _, _, _): - if active(user) { + } + case let .memberRole(user, groupInfo, _, _, _, _): + if active(user) { + await MainActor.run { m.updateGroup(groupInfo) } - case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileStart(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileComplete(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileSndCancelled(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .rcvFileProgressXFTP(user, aChatItem, _, _): - chatItemSimpleUpdate(user, aChatItem) - case let .rcvFileError(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .sndFileStart(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - case let .sndFileComplete(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupDirectFile(aChatItem) - case let .sndFileRcvCancelled(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupDirectFile(aChatItem) - case let .sndFileProgressXFTP(user, aChatItem, _, _, _): - chatItemSimpleUpdate(user, aChatItem) - case let .sndFileCompleteXFTP(user, aChatItem, _): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .sndFileError(user, aChatItem): - chatItemSimpleUpdate(user, aChatItem) - cleanupFile(aChatItem) - case let .callInvitation(invitation): - m.callInvitations[invitation.contact.id] = invitation - activateCall(invitation) - case let .callOffer(_, contact, callType, offer, sharedKey, _): - withCall(contact) { call in - call.callState = .offerReceived - call.peerMedia = callType.media - call.sharedKey = sharedKey - let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) - let iceServers = getIceServers() - logger.debug(".callOffer useRelay \(useRelay)") - logger.debug(".callOffer iceServers \(String(describing: iceServers))") - m.callCommand = .offer( - offer: offer.rtcSession, - iceCandidates: offer.rtcIceCandidates, - media: callType.media, aesKey: sharedKey, - iceServers: iceServers, - relay: useRelay - ) - } - case let .callAnswer(_, contact, answer): - withCall(contact) { call in - call.callState = .answerReceived - m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates) - } - case let .callExtraInfo(_, contact, extraInfo): - withCall(contact) { _ in - m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates) - } - case let .callEnded(_, contact): - if let invitation = m.callInvitations.removeValue(forKey: contact.id) { - CallController.shared.reportCallRemoteEnded(invitation: invitation) - } - withCall(contact) { call in - m.callCommand = .end - CallController.shared.reportCallRemoteEnded(call: call) - } - case .chatSuspended: - chatSuspended() - case let .contactSwitch(_, contact, switchProgress): - m.updateContactConnectionStats(contact, switchProgress.connectionStats) - case let .groupMemberSwitch(_, groupInfo, member, switchProgress): - m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) - case let .contactRatchetSync(_, contact, ratchetSyncProgress): - m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) - case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): - m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) - default: - logger.debug("unsupported event: \(res.responseType)") } - - func withCall(_ contact: Contact, _ perform: (Call) -> Void) { - if let call = m.activeCall, call.contact.apiId == contact.apiId { - perform(call) - } else { - logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)") + case let .newMemberContactReceivedInv(user, contact, _, _): + if active(user) { + await MainActor.run { + m.updateContact(contact) } } + case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileStart(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileComplete(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileSndCancelled(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .rcvFileProgressXFTP(user, aChatItem, _, _): + await chatItemSimpleUpdate(user, aChatItem) + case let .rcvFileError(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .sndFileStart(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + case let .sndFileComplete(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupDirectFile(aChatItem) } + case let .sndFileRcvCancelled(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupDirectFile(aChatItem) } + case let .sndFileProgressXFTP(user, aChatItem, _, _, _): + await chatItemSimpleUpdate(user, aChatItem) + case let .sndFileCompleteXFTP(user, aChatItem, _): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .sndFileError(user, aChatItem): + await chatItemSimpleUpdate(user, aChatItem) + Task { cleanupFile(aChatItem) } + case let .callInvitation(invitation): + await MainActor.run { + m.callInvitations[invitation.contact.id] = invitation + } + activateCall(invitation) + case let .callOffer(_, contact, callType, offer, sharedKey, _): + await withCall(contact) { call in + call.callState = .offerReceived + call.peerMedia = callType.media + call.sharedKey = sharedKey + let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) + let iceServers = getIceServers() + logger.debug(".callOffer useRelay \(useRelay)") + logger.debug(".callOffer iceServers \(String(describing: iceServers))") + m.callCommand = .offer( + offer: offer.rtcSession, + iceCandidates: offer.rtcIceCandidates, + media: callType.media, aesKey: sharedKey, + iceServers: iceServers, + relay: useRelay + ) + } + case let .callAnswer(_, contact, answer): + await withCall(contact) { call in + call.callState = .answerReceived + m.callCommand = .answer(answer: answer.rtcSession, iceCandidates: answer.rtcIceCandidates) + } + case let .callExtraInfo(_, contact, extraInfo): + await withCall(contact) { _ in + m.callCommand = .ice(iceCandidates: extraInfo.rtcIceCandidates) + } + case let .callEnded(_, contact): + if let invitation = await MainActor.run(body: { m.callInvitations.removeValue(forKey: contact.id) }) { + CallController.shared.reportCallRemoteEnded(invitation: invitation) + } + await withCall(contact) { call in + m.callCommand = .end + CallController.shared.reportCallRemoteEnded(call: call) + } + case .chatSuspended: + chatSuspended() + case let .contactSwitch(_, contact, switchProgress): + await MainActor.run { + m.updateContactConnectionStats(contact, switchProgress.connectionStats) + } + case let .groupMemberSwitch(_, groupInfo, member, switchProgress): + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) + } + case let .contactRatchetSync(_, contact, ratchetSyncProgress): + await MainActor.run { + m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) + } + case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) + } + default: + logger.debug("unsupported event: \(res.responseType)") + } + + func withCall(_ contact: Contact, _ perform: (Call) -> Void) async { + if let call = m.activeCall, call.contact.apiId == contact.apiId { + await MainActor.run { perform(call) } + } else { + logger.debug("processReceivedMsg: ignoring \(res.responseType), not in call with the contact \(contact.id)") + } } } -func active(_ user: User) -> Bool { - user.id == ChatModel.shared.currentUser?.id +func active(_ user: any UserLike) -> Bool { + user.userId == ChatModel.shared.currentUser?.id } -func chatItemSimpleUpdate(_ user: User, _ aChatItem: AChatItem) { +func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async { let m = ChatModel.shared let cInfo = aChatItem.chatInfo let cItem = aChatItem.chatItem - if active(user) && m.upsertChatItem(cInfo, cItem) { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + if active(user) { + if await MainActor.run(body: { m.upsertChatItem(cInfo, cItem) }) { + NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) + } } } -func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) { +func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async { let m = ChatModel.shared - for c in contactRefs { - m.networkStatuses[c.agentConnId] = status + await MainActor.run { + for c in contactRefs { + m.networkStatuses[c.agentConnId] = status + } } } @@ -1561,7 +1670,9 @@ func activateCall(_ callInvitation: RcvCallInvitation) { let m = ChatModel.shared CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in if let error = error { - m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + DispatchQueue.main.async { + m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + } logger.error("reportNewIncomingCall error: \(error.localizedDescription)") } else { logger.debug("reportNewIncomingCall success") @@ -1573,15 +1684,3 @@ private struct UserResponse: Decodable { var user: User? var error: String? } - -struct RuntimeError: Error { - let message: String - - init(_ message: String) { - self.message = message - } - - public var localizedDescription: String { - return message - } -} diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 5184808bd7..13e681ae25 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -139,10 +139,10 @@ struct SimpleXApp: App { let chat = chatModel.getChat(id) { loadChat(chat: chat) } - if let chatId = chatModel.ntfContactRequest { + if let ncr = chatModel.ntfContactRequest { chatModel.ntfContactRequest = nil - if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo { - Task { await acceptContactRequest(contactRequest) } + if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo { + Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) } } } } catch let error { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 5bbc64e165..ec4cb90097 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -143,7 +143,12 @@ struct ChatInfoView: View { if let customUserProfile = customUserProfile { Section("Incognito") { - infoRow("Your random profile", customUserProfile.chatViewName) + HStack { + Text("Your random profile") + Spacer() + Text(customUserProfile.chatViewName) + .foregroundStyle(.indigo) + } } } @@ -159,6 +164,7 @@ struct ChatInfoView: View { // synchronizeConnectionButtonForce() // } } + .disabled(!contact.ready) if let contactLink = contact.contactLink { Section { @@ -175,30 +181,33 @@ struct ChatInfoView: View { } } - Section("Servers") { - networkStatusRow() - .onTapGesture { - alert = .networkStatusAlert - } - if let connStats = connectionStats { - Button("Change receiving address") { - alert = .switchAddressAlert - } - .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } - || connStats.ratchetSyncSendProhibited - ) - if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { - Button("Abort changing address") { - alert = .abortSwitchAddressAlert + if contact.ready { + Section("Servers") { + networkStatusRow() + .onTapGesture { + alert = .networkStatusAlert + } + if let connStats = connectionStats { + Button("Change receiving address") { + alert = .switchAddressAlert } .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + !contact.ready + || connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } || connStats.ratchetSyncSendProhibited ) + if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { + Button("Abort changing address") { + alert = .abortSwitchAddressAlert + } + .disabled( + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + || connStats.ratchetSyncSendProhibited + ) + } + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) } } @@ -431,9 +440,9 @@ struct ChatInfoView: View { do { try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) await MainActor.run { - chatModel.removeChat(chat.chatInfo.id) - chatModel.chatId = nil dismiss() + chatModel.chatId = nil + chatModel.removeChat(chat.chatInfo.id) } } catch let error { logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index cd059b704c..9b372154ed 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -10,20 +10,11 @@ import SwiftUI import SimpleXChat struct CIEventView: View { - var chatItem: ChatItem + var eventText: Text var body: some View { HStack(alignment: .bottom, spacing: 0) { - if let member = chatItem.memberDisplayName { - Text(member) - .font(.caption) - .foregroundColor(.secondary) - .fontWeight(.light) - + Text(" ") - + chatEventText(chatItem) - } else { - chatEventText(chatItem) - } + eventText } .padding(.leading, 6) .padding(.bottom, 6) @@ -31,20 +22,8 @@ struct CIEventView: View { } } -func chatEventText(_ ci: ChatItem) -> Text { - Text(ci.content.text) - .font(.caption) - .foregroundColor(.secondary) - .fontWeight(.light) - + Text(" ") - + ci.timestampText - .font(.caption) - .foregroundColor(Color.secondary) - .fontWeight(.light) -} - struct CIEventView_Previews: PreviewProvider { static var previews: some View { - CIEventView(chatItem: ChatItem.getGroupEventSample()) + CIEventView(eventText: Text("event happened")) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index 6e792e825d..359633a5f5 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -16,8 +16,8 @@ struct CIFileView: View { var body: some View { let metaReserve = edited - ? " " - : " " + ? " " + : " " Button(action: fileAction) { HStack(alignment: .bottom, spacing: 6) { fileIndicator() @@ -84,7 +84,8 @@ struct CIFileView: View { Task { logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") if let user = ChatModel.shared.currentUser { - await receiveFile(user: user, fileId: file.fileId) + let encrypted = privacyEncryptLocalFilesGroupDefault.get() + await receiveFile(user: user, fileId: file.fileId, encrypted: encrypted) } } } else { @@ -109,9 +110,8 @@ struct CIFileView: View { } case .rcvComplete: logger.debug("CIFileView fileAction - in .rcvComplete") - if let filePath = getLoadedFilePath(file) { - let url = URL(fileURLWithPath: filePath) - showShareSheet(items: [url]) + if let fileSource = getLoadedFileSource(file) { + saveCryptoFile(fileSource) } default: break } @@ -193,11 +193,35 @@ struct CIFileView: View { } } +func saveCryptoFile(_ fileSource: CryptoFile) { + if let cfArgs = fileSource.cryptoArgs { + let url = getAppFilePath(fileSource.filePath) + let tempUrl = getTempFilesDirectory().appendingPathComponent(fileSource.filePath) + Task { + do { + try decryptCryptoFile(fromPath: url.path, cryptoArgs: cfArgs, toPath: tempUrl.path) + await MainActor.run { + showShareSheet(items: [tempUrl]) { + removeFile(tempUrl) + } + } + } catch { + await MainActor.run { + AlertManager.shared.showAlertMsg(title: "Error decrypting file", message: "Error: \(error.localizedDescription)") + } + } + } + } else { + let url = getAppFilePath(fileSource.filePath) + showShareSheet(items: [url]) + } +} + struct CIFileView_Previews: PreviewProvider { static var previews: some View { let sentFile: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .file("")), quotedItem: nil, file: CIFile.getSample(fileStatus: .sndComplete) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index b13ee52829..bb43179577 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -16,6 +16,7 @@ struct CIImageView: View { let maxWidth: CGFloat @Binding var imgWidth: CGFloat? @State var scrollProxy: ScrollViewProxy? + @State var metaColor: Color @State private var showFullScreenImage = false var body: some View { @@ -36,9 +37,8 @@ struct CIImageView: View { case .rcvInvitation: Task { if let user = ChatModel.shared.currentUser { - await receiveFile(user: user, fileId: file.fileId) + await receiveFile(user: user, fileId: file.fileId, encrypted: chatItem.encryptLocalFile) } - // TODO image accepted alert? } case .rcvAccepted: switch file.fileProtocol { @@ -110,7 +110,7 @@ struct CIImageView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(.white) + .foregroundColor(metaColor) .padding(padding) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift new file mode 100644 index 0000000000..a204d83f1e --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -0,0 +1,79 @@ +// +// CIMemberCreatedContactView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 19.09.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct CIMemberCreatedContactView: View { + var chatItem: ChatItem + + var body: some View { + HStack(alignment: .bottom, spacing: 0) { + switch chatItem.chatDir { + case let .groupRcv(groupMember): + if let contactId = groupMember.memberContactId { + memberCreatedContactView(openText: "Open") + .onTapGesture { + dismissAllSheets(animated: true) + DispatchQueue.main.async { + ChatModel.shared.chatId = "@\(contactId)" + } + } + } else { + memberCreatedContactView() + } + default: + EmptyView() + } + } + .padding(.leading, 6) + .padding(.bottom, 6) + .textSelection(.disabled) + } + + private func memberCreatedContactView(openText: LocalizedStringKey? = nil) -> some View { + var r = eventText() + if let openText { + r = r + + Text(openText) + .fontWeight(.medium) + .foregroundColor(.accentColor) + + Text(" ") + } + r = r + chatItem.timestampText + .fontWeight(.light) + .foregroundColor(.secondary) + return r.font(.caption) + } + + private func eventText() -> Text { + if let member = chatItem.memberDisplayName { + return Text(member + " " + chatItem.content.text + " ") + .fontWeight(.light) + .foregroundColor(.secondary) + } else { + return Text(chatItem.content.text + " ") + .fontWeight(.light) + .foregroundColor(.secondary) + } + } +} + +struct CIMemberCreatedContactView_Previews: PreviewProvider { + static var previews: some View { + let content = CIContent.rcvGroupEvent(rcvGroupEvent: .memberCreatedContact) + let chatItem = ChatItem( + chatDir: .groupRcv(groupMember: GroupMember.sampleData), + meta: CIMeta.getSample(1, .now, content.text, .rcvRead), + content: content, + quotedItem: nil, + file: nil + ) + CIMemberCreatedContactView(chatItem: chatItem) + } +} diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index de7b3e251e..30430dc19a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -13,6 +13,7 @@ struct CIMetaView: View { @EnvironmentObject var chat: Chat var chatItem: ChatItem var metaColor = Color.secondary + var paleMetaColor = Color(UIColor.tertiaryLabel) var body: some View { if chatItem.isDeletedContent { @@ -20,16 +21,28 @@ struct CIMetaView: View { } else { let meta = chatItem.meta let ttl = chat.chatInfo.timedMessagesTTL + let encrypted = chatItem.encryptedFile switch meta.itemStatus { - case .sndSent: - ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .sent) - case .sndRcvd: - ZStack { - ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd1) - ciMetaText(meta, chatTTL: ttl, color: metaColor, sent: .rcvd2) + case let .sndSent(sndProgress): + switch sndProgress { + case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent) + case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent) + } + case let .sndRcvd(_, sndProgress): + switch sndProgress { + case .complete: + ZStack { + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2) + } + case .partial: + ZStack { + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2) + } } default: - ciMetaText(meta, chatTTL: ttl, color: metaColor) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor) } } } @@ -41,7 +54,7 @@ enum SentCheckmark { case rcvd2 } -func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text { +func ciMetaText(_ meta: CIMeta, chatTTL: Int?, encrypted: Bool?, color: Color = .clear, transparent: Bool = false, sent: SentCheckmark? = nil) -> Text { var r = Text("") if meta.itemEdited { r = r + statusIconText("pencil", color) @@ -61,14 +74,18 @@ func ciMetaText(_ meta: CIMeta, chatTTL: Int?, color: Color = .clear, transparen switch sent { case nil: r = r + t1 case .sent: r = r + t1 + gap - case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : color.opacity(0.67)) + gap + case .rcvd1: r = r + t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) + gap case .rcvd2: r = r + gap + t1 } r = r + Text(" ") } else if !meta.disappearing { r = r + statusIconText("circlebadge.fill", .clear) + Text(" ") } - return (r + meta.timestampText.foregroundColor(color)).font(.caption) + if let enc = encrypted { + r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ") + } + r = r + meta.timestampText.foregroundColor(color) + return r.font(.caption) } private func statusIconText(_ icon: String, _ color: Color) -> Text { @@ -78,8 +95,12 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text { struct CIMetaView_Previews: PreviewProvider { static var previews: some View { Group { - CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent)) - CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, itemEdited: true)) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete))) + CIMetaView(chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true)) CIMetaView(chatItem: ChatItem.getDeletedContentSample()) } .previewLayout(.fixed(width: 360, height: 100)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index f4f8a52eb2..e1a5c252ec 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -16,7 +16,6 @@ struct CIRcvDecryptionError: View { var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem - var showMember = false @State private var alert: CIRcvDecryptionErrorAlert? enum CIRcvDecryptionErrorAlert: Identifiable { @@ -106,9 +105,6 @@ struct CIRcvDecryptionError: View { ZStack(alignment: .bottomTrailing) { VStack(alignment: .leading, spacing: 2) { HStack { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") - } Text(chatItem.content.text) .foregroundColor(.red) .italic() @@ -122,7 +118,7 @@ struct CIRcvDecryptionError: View { .foregroundColor(syncSupported ? .accentColor : .secondary) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) ) } .padding(.horizontal, 12) @@ -137,20 +133,13 @@ struct CIRcvDecryptionError: View { } private func decryptionErrorItem(_ onClick: @escaping (() -> Void)) -> some View { - func text() -> Text { - Text(chatItem.content.text) - .foregroundColor(.red) - .italic() - + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, transparent: true) - } return ZStack(alignment: .bottomTrailing) { HStack { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") + text() - } else { - text() - } + Text(chatItem.content.text) + .foregroundColor(.red) + .italic() + + Text(" ") + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) } .padding(.horizontal, 12) CIMetaView(chatItem: chatItem) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index 4387614918..3807a11b4e 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -22,6 +22,7 @@ struct CIVideoView: View { @State private var scrollProxy: ScrollViewProxy? @State private var preview: UIImage? = nil @State private var player: AVPlayer? + @State private var fullPlayer: AVPlayer? @State private var url: URL? @State private var showFullScreenPlayer = false @State private var timeObserver: Any? = nil @@ -36,6 +37,7 @@ struct CIVideoView: View { self.scrollProxy = scrollProxy if let url = getLoadedVideo(chatItem.file) { self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(url, false)) + self._fullPlayer = State(initialValue: AVPlayer(url: url)) self._url = State(initialValue: url) } if let data = Data(base64Encoded: dropImagePrefix(image)), @@ -57,7 +59,7 @@ struct CIVideoView: View { if let file = file { switch file.fileStatus { case .rcvInvitation: - receiveFileIfValidSize(file: file, receiveFile: receiveFile) + receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile) case .rcvAccepted: switch file.fileProtocol { case .xftp: @@ -83,7 +85,7 @@ struct CIVideoView: View { } if let file = file, case .rcvInvitation = file.fileStatus { Button { - receiveFileIfValidSize(file: file, receiveFile: receiveFile) + receiveFileIfValidSize(file: file, encrypted: false, receiveFile: receiveFile) } label: { playPauseIcon("play.fill") } @@ -96,6 +98,7 @@ struct CIVideoView: View { DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { ZStack(alignment: .center) { + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete VideoPlayerView(player: player, url: url, showControls: false) .frame(width: w, height: w * preview.size.height / preview.size.width) .onChange(of: ChatModel.shared.stopPreviousRecPlay) { playingUrl in @@ -113,7 +116,9 @@ struct CIVideoView: View { player.pause() videoPlaying = false case .paused: - showFullScreenPlayer = true + if canBePlayed { + showFullScreenPlayer = true + } default: () } } @@ -122,8 +127,9 @@ struct CIVideoView: View { ChatModel.shared.stopPreviousRecPlay = url player.play() } label: { - playPauseIcon("play.fill") + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") } + .disabled(!canBePlayed) } } loadingIndicator() @@ -247,10 +253,11 @@ struct CIVideoView: View { .padding([.trailing, .top], 11) } - private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64) async -> Void) { + // TODO encrypt: where file size is checked? + private func receiveFileIfValidSize(file: CIFile, encrypted: Bool, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) { Task { if let user = ChatModel.shared.currentUser { - await receiveFile(user, file.fileId) + await receiveFile(user, file.fileId, encrypted, false) } } } @@ -258,8 +265,7 @@ struct CIVideoView: View { private func fullScreenPlayer(_ url: URL) -> some View { ZStack { Color.black.edgesIgnoringSafeArea(.all) - VideoPlayer(player: createFullScreenPlayerAndPlay(url)) { - } + VideoPlayer(player: fullPlayer) .overlay(alignment: .topLeading, content: { Button(action: { showFullScreenPlayer = false }, label: { @@ -282,28 +288,29 @@ struct CIVideoView: View { } } ) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now()) { + ChatModel.shared.stopPreviousRecPlay = url + if let player = fullPlayer { + player.play() + fullScreenTimeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in + player.seek(to: CMTime.zero) + player.play() + } + } + } + } .onDisappear { if let fullScreenTimeObserver = fullScreenTimeObserver { NotificationCenter.default.removeObserver(fullScreenTimeObserver) } fullScreenTimeObserver = nil + fullPlayer?.pause() + fullPlayer?.seek(to: CMTime.zero) } } } - private func createFullScreenPlayerAndPlay(_ url: URL) -> AVPlayer { - let player = AVPlayer(url: url) - DispatchQueue.main.asyncAfter(deadline: .now()) { - ChatModel.shared.stopPreviousRecPlay = url - player.play() - fullScreenTimeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in - player.seek(to: CMTime.zero) - player.play() - } - } - return player - } - private func addObserver(_ player: AVPlayer, _ url: URL) { timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in if let item = player.currentItem { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 9541ac9483..b0875abe8d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -159,7 +159,8 @@ struct VoiceMessagePlayer: View { } } .onChange(of: chatModel.stopPreviousRecPlay) { it in - if let recordingFileName = getLoadedFileName(recordingFile), chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) { + if let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, + chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) { audioPlayer?.stop() playbackState = .noPlayback playbackTime = TimeInterval(0) @@ -174,8 +175,8 @@ struct VoiceMessagePlayer: View { switch playbackState { case .noPlayback: Button { - if let recordingFileName = getLoadedFileName(recordingFile) { - startPlayback(recordingFileName) + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) } } label: { playPauseIcon("play.fill") @@ -219,7 +220,7 @@ struct VoiceMessagePlayer: View { Button { Task { if let user = ChatModel.shared.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId) + await receiveFile(user: user, fileId: recordingFile.fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get()) } } } label: { @@ -251,8 +252,8 @@ struct VoiceMessagePlayer: View { .clipShape(Circle()) } - private func startPlayback(_ recordingFileName: String) { - chatModel.stopPreviousRecPlay = getAppFilePath(recordingFileName) + private func startPlayback(_ recordingSource: CryptoFile) { + chatModel.stopPreviousRecPlay = getAppFilePath(recordingSource.filePath) audioPlayer = AudioPlayer( onTimer: { playbackTime = $0 }, onFinishPlayback: { @@ -260,7 +261,7 @@ struct VoiceMessagePlayer: View { playbackTime = TimeInterval(0) } ) - audioPlayer?.start(fileName: recordingFileName, at: playbackTime) + audioPlayer?.start(fileSource: recordingSource, at: playbackTime) playbackState = .playing } } @@ -269,7 +270,7 @@ struct CIVoiceView_Previews: PreviewProvider { static var previews: some View { let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)), quotedItem: nil, file: CIFile.getSample(fileStatus: .sndComplete) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift index 80bb7ea1ef..af4df40978 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift @@ -12,13 +12,9 @@ import SimpleXChat struct DeletedItemView: View { @Environment(\.colorScheme) var colorScheme var chatItem: ChatItem - var showMember = false var body: some View { HStack(alignment: .bottom, spacing: 0) { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") - } Text(chatItem.content.text) .foregroundColor(.secondary) .italic() @@ -37,10 +33,7 @@ struct DeletedItemView_Previews: PreviewProvider { static var previews: some View { Group { DeletedItemView(chatItem: ChatItem.getDeletedContentSample()) - DeletedItemView( - chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData)), - showMember: true - ) + DeletedItemView(chatItem: ChatItem.getDeletedContentSample(dir: .groupRcv(groupMember: GroupMember.sampleData))) } .previewLayout(.fixed(width: 360, height: 200)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index e45b5bd183..f5ae761e84 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -32,7 +32,7 @@ func emojiText(_ text: String) -> Text { struct EmojiItemView_Previews: PreviewProvider { static var previews: some View { Group{ - EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent)) + EmojiItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete))) EmojiItemView(chatItem: ChatItem.getSample(2, .directRcv, .now, "👍")) } .previewLayout(.fixed(width: 360, height: 70)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index 4446131a75..3f7ca3f836 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -75,14 +75,14 @@ struct FramedCIVoiceView_Previews: PreviewProvider { static var previews: some View { let sentVoiceMessage: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .voice(text: "Hello there", duration: 30)), quotedItem: nil, file: CIFile.getSample(fileStatus: .sndComplete) ) let voiceMessageWithQuote: ChatItem = ChatItem( chatDir: .directSnd, - meta: CIMeta.getSample(1, .now, "", .sndSent, itemEdited: true), + meta: CIMeta.getSample(1, .now, "", .sndSent(sndProgress: .complete), itemEdited: true), content: .sndMsgContent(msgContent: .voice(text: "", duration: 30)), quotedItem: CIQuote.getSample(1, .now, "Hi", chatDir: .directRcv), file: CIFile.getSample(fileStatus: .sndComplete) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 9888ae7e8e..aab0cd5f55 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -18,7 +18,6 @@ struct FramedItemView: View { @Environment(\.colorScheme) var colorScheme var chatInfo: ChatInfo var chatItem: ChatItem - var showMember = false var maxWidth: CGFloat = .infinity @State var scrollProxy: ScrollViewProxy? = nil @State var msgWidth: CGFloat = 0 @@ -57,7 +56,7 @@ struct FramedItemView: View { } } - ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: framedMsgContentView) + ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: framedMsgContentView) .padding(chatItem.content.msgContent != nil ? 0 : 4) .overlay(DetermineWidth()) } @@ -68,6 +67,7 @@ struct FramedItemView: View { .padding(.horizontal, 12) .padding(.bottom, 6) .overlay(DetermineWidth()) + .accessibilityLabel("") } } .background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme)) @@ -97,7 +97,7 @@ struct FramedItemView: View { } else { switch (chatItem.content.msgContent) { case let .image(text, image): - CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy) + CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear @@ -107,7 +107,7 @@ struct FramedItemView: View { value: .white ) } else { - ciMsgContentView (chatItem, showMember) + ciMsgContentView(chatItem) } case let .video(text, image, duration): CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy) @@ -120,27 +120,27 @@ struct FramedItemView: View { value: .white ) } else { - ciMsgContentView (chatItem, showMember) + ciMsgContentView(chatItem) } case let .voice(text, duration): FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) .overlay(DetermineWidth()) if text != "" { - ciMsgContentView (chatItem, showMember) + ciMsgContentView(chatItem) } case let .file(text): ciFileView(chatItem, text) case let .link(_, preview): CILinkView(linkPreview: preview) - ciMsgContentView (chatItem, showMember) + ciMsgContentView(chatItem) case let .unknown(_, text: text): if chatItem.file == nil { - ciMsgContentView (chatItem, showMember) + ciMsgContentView(chatItem) } else { ciFileView(chatItem, text) } default: - ciMsgContentView (chatItem, showMember) + ciMsgContentView(chatItem) } } } @@ -232,17 +232,27 @@ struct FramedItemView: View { } private func ciQuotedMsgView(_ qi: CIQuote) -> some View { - MsgContentView( - text: qi.text, - formattedText: qi.formattedText, - sender: qi.getSender(membership()) - ) - .lineLimit(3) - .font(.subheadline) - .padding(.vertical, 6) + Group { + if let sender = qi.getSender(membership()) { + VStack(alignment: .leading, spacing: 2) { + Text(sender).font(.caption).foregroundColor(.secondary) + ciQuotedMsgTextView(qi, lines: 2) + } + } else { + ciQuotedMsgTextView(qi, lines: 3) + } + } + .padding(.top, 6) .padding(.horizontal, 12) } + private func ciQuotedMsgTextView(_ qi: CIQuote, lines: Int) -> some View { + MsgContentView(text: qi.text, formattedText: qi.formattedText) + .lineLimit(lines) + .font(.subheadline) + .padding(.bottom, 6) + } + private func ciQuoteIconView(_ image: String) -> some View { Image(systemName: image) .resizable() @@ -260,13 +270,12 @@ struct FramedItemView: View { } } - @ViewBuilder private func ciMsgContentView(_ ci: ChatItem, _ showMember: Bool = false) -> some View { + @ViewBuilder private func ciMsgContentView(_ ci: ChatItem) -> some View { let text = ci.meta.isLive ? ci.content.msgContent?.text ?? ci.text : ci.text let rtl = isRightToLeft(text) let v = MsgContentView( text: text, formattedText: text == "" ? [] : ci.formattedText, - sender: showMember ? ci.memberDisplayName : nil, meta: ci.meta, rightToLeft: rtl ) @@ -288,7 +297,7 @@ struct FramedItemView: View { CIFileView(file: chatItem.file, edited: chatItem.meta.itemEdited) .overlay(DetermineWidth()) if text != "" || ci.meta.isLive { - ciMsgContentView (chatItem, showMember) + ciMsgContentView (chatItem) } } @@ -349,8 +358,8 @@ struct FramedItemView_Previews: PreviewProvider { Group{ FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) @@ -363,10 +372,10 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) @@ -381,10 +390,10 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent, quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent, quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) FramedItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 1ab631232e..9908d4d104 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -12,10 +12,9 @@ import SimpleXChat struct IntegrityErrorItemView: View { var msgError: MsgErrorType var chatItem: ChatItem - var showMember = false var body: some View { - CIMsgError(chatItem: chatItem, showMember: showMember) { + CIMsgError(chatItem: chatItem) { switch msgError { case .msgSkipped: AlertManager.shared.showAlertMsg( @@ -54,14 +53,10 @@ struct IntegrityErrorItemView: View { struct CIMsgError: View { var chatItem: ChatItem - var showMember = false var onTap: () -> Void var body: some View { HStack(alignment: .bottom, spacing: 0) { - if showMember, let member = chatItem.memberDisplayName { - Text(member).fontWeight(.medium) + Text(": ") - } Text(chatItem.content.text) .foregroundColor(.red) .italic() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index 1442f1a2a3..8e042a8c76 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -12,13 +12,9 @@ import SimpleXChat struct MarkedDeletedItemView: View { @Environment(\.colorScheme) var colorScheme var chatItem: ChatItem - var showMember = false var body: some View { HStack(alignment: .bottom, spacing: 0) { - if showMember, let member = chatItem.memberDisplayName { - Text(member).font(.caption).fontWeight(.medium) + Text(": ").font(.caption) - } if case let .moderated(_, byGroupMember) = chatItem.meta.itemDeleted { markedDeletedText("moderated by \(byGroupMember.chatViewName)") } else { @@ -46,7 +42,7 @@ struct MarkedDeletedItemView: View { struct MarkedDeletedItemView_Previews: PreviewProvider { static var previews: some View { Group { - MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))) + MarkedDeletedItemView(chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))) } .previewLayout(.fixed(width: 360, height: 200)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 3ac908bb78..498b3cb2e0 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -80,7 +80,7 @@ struct MsgContentView: View { } private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, transparent: true) + (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 4db32bc74f..61802c61fe 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -14,11 +14,23 @@ struct ChatItemInfoView: View { var ci: ChatItem @Binding var chatItemInfo: ChatItemInfo? @State private var selection: CIInfoTab = .history + @State private var alert: CIInfoViewAlert? = nil @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum CIInfoTab { case history case quote + case delivery + } + + enum CIInfoViewAlert: Identifiable { + case alert(title: String, text: String) + + var id: String { + switch self { + case let .alert(title, text): return "alert \(title) \(text)" + } + } } var body: some View { @@ -31,6 +43,11 @@ struct ChatItemInfoView: View { } } } + .alert(item: $alert) { a in + switch(a) { + case let .alert(title, text): return Alert(title: Text(title), message: Text(text)) + } + } } } @@ -40,19 +57,44 @@ struct ChatItemInfoView: View { : NSLocalizedString("Received message", comment: "message info title") } + private var numTabs: Int { + var numTabs = 1 + if chatItemInfo?.memberDeliveryStatuses != nil { + numTabs += 1 + } + if ci.quotedItem != nil { + numTabs += 1 + } + return numTabs + } + @ViewBuilder private func itemInfoView() -> some View { - if let qi = ci.quotedItem { + if numTabs > 1 { TabView(selection: $selection) { + if let mdss = chatItemInfo?.memberDeliveryStatuses { + deliveryTab(mdss) + .tabItem { + Label("Delivery", systemImage: "checkmark.message") + } + .tag(CIInfoTab.delivery) + } historyTab() .tabItem { Label("History", systemImage: "clock") } .tag(CIInfoTab.history) - quoteTab(qi) - .tabItem { - Label("In reply to", systemImage: "arrowshape.turn.up.left") - } - .tag(CIInfoTab.quote) + if let qi = ci.quotedItem { + quoteTab(qi) + .tabItem { + Label("In reply to", systemImage: "arrowshape.turn.up.left") + } + .tag(CIInfoTab.quote) + } + } + .onAppear { + if chatItemInfo?.memberDeliveryStatuses != nil { + selection = .delivery + } } } else { historyTab() @@ -217,9 +259,87 @@ struct ChatItemInfoView: View { : Color(uiColor: .tertiarySystemGroupedBackground) } + @ViewBuilder private func deliveryTab(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + ScrollView { + VStack(alignment: .leading, spacing: 16) { + details() + Divider().padding(.vertical) + Text("Delivery") + .font(.title2) + .padding(.bottom, 4) + memberDeliveryStatusesView(memberDeliveryStatuses) + } + .padding() + } + .frame(maxHeight: .infinity, alignment: .top) + } + + @ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { + VStack(alignment: .leading, spacing: 12) { + let mss = membersStatuses(memberDeliveryStatuses) + if !mss.isEmpty { + ForEach(mss, id: \.0.groupMemberId) { memberStatus in + memberDeliveryStatusView(memberStatus.0, memberStatus.1) + } + } else { + Text("No delivery information") + .foregroundColor(.secondary) + } + } + } + + private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { + memberDeliveryStatuses.compactMap({ mds in + if let mem = ChatModel.shared.groupMembers.first(where: { $0.groupMemberId == mds.groupMemberId }) { + return (mem, mds.memberDeliveryStatus) + } else { + return nil + } + }) + } + + private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { + HStack{ + ProfileImage(imageStr: member.image) + .frame(width: 30, height: 30) + .padding(.trailing, 2) + Text(member.chatViewName) + .lineLimit(1) + Spacer() + let v = Group { + if let (icon, statusColor) = status.statusIcon(Color.secondary) { + switch status { + case .sndRcvd: + ZStack(alignment: .trailing) { + Image(systemName: icon) + .foregroundColor(statusColor.opacity(0.67)) + .padding(.trailing, 6) + Image(systemName: icon) + .foregroundColor(statusColor.opacity(0.67)) + } + default: + Image(systemName: icon) + .foregroundColor(statusColor) + } + } else { + Image(systemName: "ellipsis") + .foregroundColor(Color.secondary) + } + } + + if let (title, text) = status.statusInfo { + v.onTapGesture { + alert = .alert(title: title, text: text) + } + } else { + v + } + } + } + private func itemInfoShareText() -> String { let meta = ci.meta - var shareText: [String] = [title, ""] + var shareText: [String] = [String.localizedStringWithFormat(NSLocalizedString("# %@", comment: "copied message info title, # "), title), ""] shareText += [String.localizedStringWithFormat(NSLocalizedString("Sent at: %@", comment: "copied message info"), localTimestamp(meta.itemTs))] if !ci.chatDir.sent { shareText += [String.localizedStringWithFormat(NSLocalizedString("Received at: %@", comment: "copied message info"), localTimestamp(meta.createdAt))] @@ -245,7 +365,7 @@ struct ChatItemInfoView: View { ] } if let qi = ci.quotedItem { - shareText += ["", NSLocalizedString("In reply to", comment: "copied message info")] + shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")] let t = qi.text shareText += [""] if let sender = qi.getSender(nil) { @@ -264,7 +384,7 @@ struct ChatItemInfoView: View { } if let chatItemInfo = chatItemInfo, !chatItemInfo.itemVersions.isEmpty { - shareText += ["", NSLocalizedString("History", comment: "copied message info")] + shareText += ["", NSLocalizedString("## History", comment: "copied message info")] for (index, itemVersion) in chatItemInfo.itemVersions.enumerated() { let t = itemVersion.msgContent.text shareText += [ diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 245b55c921..a79047ebcc 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -12,7 +12,6 @@ import SimpleXChat struct ChatItemView: View { var chatInfo: ChatInfo var chatItem: ChatItem - var showMember = false var maxWidth: CGFloat = .infinity @State var scrollProxy: ScrollViewProxy? = nil @Binding var revealed: Bool @@ -23,7 +22,6 @@ struct ChatItemView: View { init(chatInfo: ChatInfo, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, scrollProxy: ScrollViewProxy? = nil, revealed: Binding<Bool>, allowMenu: Binding<Bool> = .constant(false), audioPlayer: Binding<AudioPlayer?> = .constant(nil), playbackState: Binding<VoiceMessagePlaybackState> = .constant(.noPlayback), playbackTime: Binding<TimeInterval?> = .constant(nil)) { self.chatInfo = chatInfo self.chatItem = chatItem - self.showMember = showMember self.maxWidth = maxWidth _scrollProxy = .init(initialValue: scrollProxy) _revealed = revealed @@ -36,14 +34,14 @@ struct ChatItemView: View { var body: some View { let ci = chatItem if chatItem.meta.itemDeleted != nil && !revealed { - MarkedDeletedItemView(chatItem: chatItem, showMember: showMember) + MarkedDeletedItemView(chatItem: chatItem) } else if ci.quotedItem == nil && ci.meta.itemDeleted == nil && !ci.meta.isLive { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { EmojiItemView(chatItem: ci) } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { CIVoiceView(chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu) } else if ci.content.msgContent == nil { - ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case + ChatItemContentView(chatInfo: chatInfo, chatItem: chatItem, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case } else { framedItemView() } @@ -53,15 +51,16 @@ struct ChatItemView: View { } private func framedItemView() -> some View { - FramedItemView(chatInfo: chatInfo, chatItem: chatItem, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + FramedItemView(chatInfo: chatInfo, chatItem: chatItem, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) } } struct ChatItemContentView<Content: View>: View { + @EnvironmentObject var chatModel: ChatModel var chatInfo: ChatInfo var chatItem: ChatItem - var showMember: Bool var msgContentView: () -> Content + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false var body: some View { switch chatItem.content { @@ -71,10 +70,17 @@ struct ChatItemContentView<Content: View>: View { case .rcvDeleted: deletedItemView() case let .sndCall(status, duration): callItemView(status, duration) case let .rcvCall(status, duration): callItemView(status, duration) - case let .rcvIntegrityError(msgError): IntegrityErrorItemView(msgError: msgError, chatItem: chatItem, showMember: showMember) - case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem, showMember: showMember) + case let .rcvIntegrityError(msgError): + if developerTools { + IntegrityErrorItemView(msgError: msgError, chatItem: chatItem) + } else { + ZStack {} + } + case let .rcvDecryptionError(msgDecryptError, msgCount): CIRcvDecryptionError(msgDecryptError: msgDecryptError, msgCount: msgCount, chatItem: chatItem) case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) + case .rcvGroupEvent(.memberConnected): CIEventView(eventText: membersConnectedItemText) + case .rcvGroupEvent(.memberCreatedContact): CIMemberCreatedContactView(chatItem: chatItem) case .rcvGroupEvent: eventItemView() case .sndGroupEvent: eventItemView() case .rcvConnEvent: eventItemView() @@ -96,7 +102,7 @@ struct ChatItemContentView<Content: View>: View { } private func deletedItemView() -> some View { - DeletedItemView(chatItem: chatItem, showMember: showMember) + DeletedItemView(chatItem: chatItem) } private func callItemView(_ status: CICallStatus, _ duration: Int) -> some View { @@ -108,12 +114,54 @@ struct ChatItemContentView<Content: View>: View { } private func eventItemView() -> some View { - CIEventView(chatItem: chatItem) + return CIEventView(eventText: eventItemViewText()) + } + + private func eventItemViewText() -> Text { + if let member = chatItem.memberDisplayName { + return Text(member + " ") + .font(.caption) + .foregroundColor(.secondary) + .fontWeight(.light) + + chatEventText(chatItem) + } else { + return chatEventText(chatItem) + } } private func chatFeatureView(_ feature: Feature, _ iconColor: Color) -> some View { CIChatFeatureView(chatItem: chatItem, feature: feature, iconColor: iconColor) } + + private var membersConnectedItemText: Text { + if let t = membersConnectedText { + return chatEventText(t, chatItem.timestampText) + } else { + return eventItemViewText() + } + } + + private var membersConnectedText: LocalizedStringKey? { + let ns = chatModel.getConnectedMemberNames(chatItem) + return ns.count > 3 + ? "\(ns[0]), \(ns[1]) and \(ns.count - 2) other members connected" + : ns.count == 3 + ? "\(ns[0] + ", " + ns[1]) and \(ns[2]) connected" + : ns.count == 2 + ? "\(ns[0]) and \(ns[1]) connected" + : nil + } +} + +func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text { + (Text(eventText) + Text(" ") + ts) + .font(.caption) + .foregroundColor(.secondary) + .fontWeight(.light) +} + +func chatEventText(_ ci: ChatItem) -> Text { + chatEventText("\(ci.content.text)", ci.timestampText) } struct ChatItemView_Previews: PreviewProvider { @@ -125,9 +173,9 @@ struct ChatItemView_Previews: PreviewProvider { ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(2, .directRcv, .now, "🙂🙂🙂🙂🙂🙂"), revealed: Binding.constant(false)) ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getDeletedContentSample(), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent, itemLive: true), revealed: Binding.constant(true)) - ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemLive: true), revealed: Binding.constant(true)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(false)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "🙂", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) + ChatItemView(chatInfo: ChatInfo.sampleData.direct, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemLive: true), revealed: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 70)) .environmentObject(Chat.sampleData) diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index bd57841a79..81a063dcfc 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -64,6 +64,7 @@ struct ChatView: View { Spacer(minLength: 0) + connectingText() ComposeView( chat: chat, composeState: $composeState, @@ -149,6 +150,7 @@ struct ChatView: View { HStack { if contact.allowsFeature(.calls) { callButton(contact, .audio, imageName: "phone") + .disabled(!contact.ready) } Menu { if contact.allowsFeature(.calls) { @@ -157,9 +159,11 @@ struct ChatView: View { } label: { Label("Video call", systemImage: "video") } + .disabled(!contact.ready) } searchButton() toggleNtfsButton(chat) + .disabled(!contact.ready) } label: { Image(systemName: "ellipsis") } @@ -261,7 +265,7 @@ struct ChatView: View { return GeometryReader { g in ScrollViewReader { proxy in ScrollView { - LazyVStack(spacing: 5) { + LazyVStack(spacing: 0) { ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in let voiceNoFrame = voiceWithoutFrame(ci) let maxWidth = cInfo.chatType == .group @@ -313,6 +317,19 @@ struct ChatView: View { } .scaleEffect(x: 1, y: -1, anchor: .center) } + + @ViewBuilder private func connectingText() -> some View { + if case let .direct(contact) = chat.chatInfo, + !contact.ready, + !contact.nextSendGrpInv { + Text("connecting…") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top) + } else { + EmptyView() + } + } private func floatingButtons(_ proxy: ScrollViewProxy) -> some View { let counts = chatModel.unreadChatItemCounts(itemsInView: itemsInView) @@ -430,68 +447,77 @@ struct ChatView: View { @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { if case let .groupRcv(member) = ci.chatDir, case let .group(groupInfo) = chat.chatInfo { - let prevItem = chatModel.getPrevChatItem(ci) - HStack(alignment: .top, spacing: 0) { - let showMember = prevItem == nil || showMemberImage(member, prevItem) - if showMember { - ProfileImage(imageStr: member.memberProfile.image) - .frame(width: memberImageSize, height: memberImageSize) - .onTapGesture { selectedMember = member } - .appSheet(item: $selectedMember) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true) + let (prevItem, nextItem) = chatModel.getChatItemNeighbors(ci) + if ci.memberConnected != nil && nextItem?.memberConnected != nil { + // memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView + ZStack {} // scroll doesn't work if it's EmptyView() + } else { + if prevItem == nil || showMemberImage(member, prevItem) { + VStack(alignment: .leading, spacing: 4) { + if ci.content.showMemberName { + Text(member.displayName) + .font(.caption) + .foregroundStyle(.secondary) + .padding(.leading, memberImageSize + 14) + .padding(.top, 7) } + HStack(alignment: .top, spacing: 8) { + ProfileImage(imageStr: member.memberProfile.image) + .frame(width: memberImageSize, height: memberImageSize) + .onTapGesture { selectedMember = member } + .appSheet(item: $selectedMember) { member in + GroupMemberInfoView(groupInfo: groupInfo, member: member, navigation: true) + } + chatItemWithMenu(ci, maxWidth) + } + } + .padding(.top, 5) + .padding(.trailing) + .padding(.leading, 12) } else { - Rectangle().fill(.clear) - .frame(width: memberImageSize, height: memberImageSize) + chatItemWithMenu(ci, maxWidth) + .padding(.top, 5) + .padding(.trailing) + .padding(.leading, memberImageSize + 8 + 12) } - ChatItemWithMenu( - ci: ci, - showMember: showMember, - maxWidth: maxWidth, - scrollProxy: scrollProxy, - deleteMessage: deleteMessage, - deletingItem: $deletingItem, - composeState: $composeState, - showDeleteMessage: $showDeleteMessage - ) - .padding(.leading, 8) - .environmentObject(chat) } - .padding(.trailing) - .padding(.leading, 12) } else { - ChatItemWithMenu( - ci: ci, - maxWidth: maxWidth, - scrollProxy: scrollProxy, - deleteMessage: deleteMessage, - deletingItem: $deletingItem, - composeState: $composeState, - showDeleteMessage: $showDeleteMessage - ) - .padding(.horizontal) - .environmentObject(chat) + chatItemWithMenu(ci, maxWidth) + .padding(.horizontal) + .padding(.top, 5) } } - + + private func chatItemWithMenu(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { + ChatItemWithMenu( + ci: ci, + maxWidth: maxWidth, + scrollProxy: scrollProxy, + deleteMessage: deleteMessage, + deletingItem: $deletingItem, + composeState: $composeState, + showDeleteMessage: $showDeleteMessage + ) + .environmentObject(chat) + } + private struct ChatItemWithMenu: View { @EnvironmentObject var chat: Chat @Environment(\.colorScheme) var colorScheme var ci: ChatItem - var showMember: Bool = false var maxWidth: CGFloat var scrollProxy: ScrollViewProxy? var deleteMessage: (CIDeleteMode) -> Void @Binding var deletingItem: ChatItem? @Binding var composeState: ComposeState @Binding var showDeleteMessage: Bool - + @State private var revealed = false @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? - + @State private var allowMenu: Bool = true - + @State private var audioPlayer: AudioPlayer? @State private var playbackState: VoiceMessagePlaybackState = .noPlayback @State private var playbackTime: TimeInterval? @@ -504,8 +530,9 @@ struct ChatView: View { ) VStack(alignment: alignment.horizontal, spacing: 3) { - ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) .uiKitContextMenu(menu: uiMenu, allowMenu: $allowMenu) + .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions() .padding(.bottom, 4) @@ -591,15 +618,15 @@ struct ChatView: View { } menu.append(shareUIAction()) menu.append(copyUIAction()) - if let filePath = getLoadedFilePath(ci.file) { + if let fileSource = getLoadedFileSource(ci.file) { if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { if image.imageData != nil { - menu.append(saveFileAction(filePath)) + menu.append(saveFileAction(fileSource)) } else { menu.append(saveImageAction(image)) } } else { - menu.append(saveFileAction(filePath)) + menu.append(saveFileAction(fileSource)) } } if ci.meta.editable && !mc.isVoice && !live { @@ -737,13 +764,12 @@ struct ChatView: View { } } - private func saveFileAction(_ filePath: String) -> UIAction { + private func saveFileAction(_ fileSource: CryptoFile) -> UIAction { UIAction( title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") + image: UIImage(systemName: fileSource.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open") ) { _ in - let fileURL = URL(fileURLWithPath: filePath) - showShareSheet(items: [fileURL]) + saveCryptoFile(fileSource) } } @@ -770,6 +796,12 @@ struct ChatView: View { await MainActor.run { chatItemInfo = ciInfo } + if case let .group(gInfo) = chat.chatInfo { + let groupMembers = await apiListMembers(gInfo.groupId) + await MainActor.run { + ChatModel.shared.groupMembers = groupMembers + } + } } catch let error { logger.error("apiGetChatItemInfo error: \(responseError(error))") } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 1d47227094..3328da8dbe 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -44,7 +44,6 @@ struct ComposeState { var contextItem: ComposeContextItem var voiceMessageRecordingState: VoiceMessageRecordingState var inProgress = false - var disabled = false var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) init( @@ -168,25 +167,23 @@ struct ComposeState { } func chatItemPreview(chatItem: ChatItem) -> ComposePreview { - let chatItemPreview: ComposePreview switch chatItem.content.msgContent { case .text: - chatItemPreview = .noPreview + return .noPreview case let .link(_, preview: preview): - chatItemPreview = .linkPreview(linkPreview: preview) + return .linkPreview(linkPreview: preview) case let .image(_, image): - chatItemPreview = .mediaPreviews(mediaPreviews: [(image, nil)]) + return .mediaPreviews(mediaPreviews: [(image, nil)]) case let .video(_, image, _): - chatItemPreview = .mediaPreviews(mediaPreviews: [(image, nil)]) + return .mediaPreviews(mediaPreviews: [(image, nil)]) case let .voice(_, duration): - chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration) + return .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration) case .file: let fileName = chatItem.file?.fileName ?? "" - chatItemPreview = .filePreview(fileName: fileName, file: getAppFilePath(fileName)) + return .filePreview(fileName: fileName, file: getAppFilePath(fileName)) default: - chatItemPreview = .noPreview + return .noPreview } - return chatItemPreview } enum UploadContent: Equatable { @@ -241,6 +238,7 @@ struct ComposeView: View { @State var pendingLinkUrl: URL? = nil @State var cancelledLinks: Set<String> = [] + @Environment(\.colorScheme) private var colorScheme @State private var showChooseSource = false @State private var showMediaPicker = false @State private var showTakePhoto = false @@ -255,8 +253,13 @@ struct ComposeView: View { // this is a workaround to fire an explicit event in certain cases @State private var stopPlayback: Bool = false + @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true + var body: some View { VStack(spacing: 0) { + if chat.chatInfo.contact?.nextSendGrpInv ?? false { + ContextInvitingContactMemberView() + } contextItemView() switch (composeState.editing, composeState.preview) { case (true, .filePreview): EmptyView() @@ -270,7 +273,7 @@ struct ComposeView: View { Image(systemName: "paperclip") .resizable() } - .disabled(composeState.attachmentDisabled || !chat.userCanSend) + .disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) .frame(width: 25, height: 25) .padding(.bottom, 12) .padding(.leading, 12) @@ -298,6 +301,7 @@ struct ComposeView: View { composeState.liveMessage = nil chatModel.removeLiveDummy() }, + nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false, voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, startVoiceMessageRecording: { @@ -309,7 +313,10 @@ struct ComposeView: View { allowVoiceMessagesToContact: allowVoiceMessagesToContact, timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, - keyboardVisible: $keyboardVisible + keyboardVisible: $keyboardVisible, + sendButtonColor: chat.chatInfo.incognito + ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) + : .accentColor ) .padding(.trailing, 12) .background(.background) @@ -442,7 +449,15 @@ struct ComposeView: View { } else if (composeState.inProgress) { clearCurrentDraft() } else if !composeState.empty { - saveCurrentDraft() + if case .recording = composeState.voiceMessageRecordingState { + finishVoiceMessageRecording() + if let fileName = composeState.voiceMessageRecordingFileName { + chatModel.filesToDelete.insert(getAppFilePath(fileName)) + } + } + if saveLastDraft { + saveCurrentDraft() + } } else { cancelCurrentVoiceRecording() clearCurrentDraft() @@ -606,7 +621,9 @@ struct ComposeView: View { if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) } await sending() } - if case let .editingItem(ci) = composeState.contextItem { + if chat.chatInfo.contact?.nextSendGrpInv ?? false { + await sendMemberContactInvitation() + } else if case let .editingItem(ci) = composeState.contextItem { sent = await updateMessage(ci, live: live) } else if let liveMessage = liveMessage, liveMessage.sentMsg != nil { sent = await updateMessage(liveMessage.chatItem, live: live) @@ -643,10 +660,10 @@ struct ComposeView: View { } case let .voicePreview(recordingFileName, duration): stopPlayback.toggle() - chatModel.filesToDelete.remove(getAppFilePath(recordingFileName)) - sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName, ttl: ttl) + let file = voiceCryptoFile(recordingFileName) + sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl) case let .filePreview(_, file): - if let savedFile = saveFileFromURL(file) { + if let savedFile = saveFileFromURL(file, encrypted: privacyEncryptLocalFilesGroupDefault.get()) { sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl) } } @@ -655,9 +672,19 @@ struct ComposeView: View { return sent func sending() async { - await MainActor.run { composeState.disabled = true } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if composeState.disabled { composeState.inProgress = true } + await MainActor.run { composeState.inProgress = true } + } + + func sendMemberContactInvitation() async { + do { + let mc = checkLinkPreview() + let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) + await MainActor.run { + self.chatModel.updateContact(contact) + } + } catch { + logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)") + AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))") } } @@ -717,13 +744,28 @@ struct ComposeView: View { func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { let (image, data) = imageData - if case let .video(_, url, duration) = data, let savedFile = saveFileFromURLWithoutLoad(url) { + if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) { return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl) } return nil } - func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { + func voiceCryptoFile(_ fileName: String) -> CryptoFile? { + if !privacyEncryptLocalFilesGroupDefault.get() { + return CryptoFile.plain(fileName) + } + let url = getAppFilePath(fileName) + let toFile = generateNewFileName("voice", "m4a") + let toUrl = getAppFilePath(toFile) + if let cfArgs = try? encryptCryptoFile(fromPath: url.path, toPath: toUrl.path) { + removeFile(url) + return CryptoFile(filePath: toFile, cryptoArgs: cfArgs) + } else { + return nil + } + } + + func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { if let chatItem = await apiSendMessage( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, @@ -740,7 +782,7 @@ struct ComposeView: View { return chatItem } if let file = file { - removeFile(file) + removeFile(file.filePath) } return nil } @@ -760,7 +802,7 @@ struct ComposeView: View { } } - func saveAnyImage(_ img: UploadContent) -> String? { + func saveAnyImage(_ img: UploadContent) -> CryptoFile? { switch img { case let .simpleImage(image): return saveImage(image) case let .animatedImage(image): return saveAnimImage(image) @@ -852,7 +894,6 @@ struct ComposeView: View { private func clearState(live: Bool = false) { if live { - composeState.disabled = false composeState.inProgress = false } else { composeState = ComposeState() @@ -865,12 +906,6 @@ struct ComposeView: View { } private func saveCurrentDraft() { - if case .recording = composeState.voiceMessageRecordingState { - finishVoiceMessageRecording() - if let fileName = composeState.voiceMessageRecordingFileName { - chatModel.filesToDelete.insert(getAppFilePath(fileName)) - } - } chatModel.draft = composeState chatModel.draftChatId = chat.id } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift index 2bd23f8ae7..2617bc77bc 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift @@ -188,7 +188,7 @@ struct ComposeVoiceView: View { playbackTime = recordingTime // animate progress bar to the end } ) - audioPlayer?.start(fileName: recordingFileName, at: playbackTime) + audioPlayer?.start(fileSource: CryptoFile.plain(recordingFileName), at: playbackTime) playbackState = .playing } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift new file mode 100644 index 0000000000..acb4f6d3e1 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift @@ -0,0 +1,32 @@ +// +// ContextInvitingContactMemberView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 18.09.2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct ContextInvitingContactMemberView: View { + @Environment(\.colorScheme) var colorScheme + + var body: some View { + HStack { + Image(systemName: "message") + .foregroundColor(.secondary) + Text("Send direct message to connect") + } + .padding(12) + .frame(minHeight: 50) + .frame(maxWidth: .infinity, alignment: .leading) + .background(colorScheme == .light ? sentColorLight : sentColorDark) + .padding(.top, 8) + } +} + +struct ContextInvitingContactMemberView_Previews: PreviewProvider { + static var previews: some View { + ContextInvitingContactMemberView() + } +} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index a9847afe4d..153d89fc27 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -22,13 +22,14 @@ struct ContextItemView: View { .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) .foregroundColor(.secondary) - MsgContentView( - text: contextItem.text, - formattedText: contextItem.formattedText, - sender: contextItem.memberDisplayName - ) - .multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading) - .lineLimit(3) + if let sender = contextItem.memberDisplayName { + VStack(alignment: .leading, spacing: 4) { + Text(sender).font(.caption).foregroundColor(.secondary) + msgContentView(lines: 2) + } + } else { + msgContentView(lines: 3) + } Spacer() Button { withAnimation { @@ -44,6 +45,15 @@ struct ContextItemView: View { .background(chatItemFrameColor(contextItem, colorScheme)) .padding(.top, 8) } + + private func msgContentView(lines: Int) -> some View { + MsgContentView( + text: contextItem.text, + formattedText: contextItem.formattedText + ) + .multilineTextAlignment(isRightToLeft(contextItem.text) ? .trailing : .leading) + .lineLimit(lines) + } } struct ContextItemView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index f90784234c..6eed51788e 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -17,6 +17,7 @@ struct SendMessageView: View { var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil var cancelLiveMessage: (() -> Void)? = nil + var nextSendGrpInv: Bool = false var showVoiceMessageButton: Bool = true var voiceMessageAllowed: Bool = true var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other @@ -28,6 +29,7 @@ struct SendMessageView: View { @State private var holdingVMR = false @Namespace var namespace @Binding var keyboardVisible: Bool + var sendButtonColor = Color.accentColor @State private var teHeight: CGFloat = 42 @State private var teFont: Font = .body @State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body) @@ -36,6 +38,7 @@ struct SendMessageView: View { @State private var showCustomDisappearingMessageDialogue = false @State private var showCustomTimePicker = false @State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get() + @State private var progressByTimeout = false var maxHeight: CGFloat = 360 var minHeight: CGFloat = 37 @AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false @@ -81,7 +84,7 @@ struct SendMessageView: View { } } - if composeState.inProgress { + if progressByTimeout { ProgressView() .scaleEffect(1.4) .frame(width: 31, height: 31, alignment: .center) @@ -102,12 +105,23 @@ struct SendMessageView: View { .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) .frame(height: teHeight) } + .onChange(of: composeState.inProgress) { inProgress in + if inProgress { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + progressByTimeout = composeState.inProgress + } + } else { + progressByTimeout = false + } + } .padding(.vertical, 8) } @ViewBuilder private func composeActionButtons() -> some View { let vmrs = composeState.voiceMessageRecordingState - if showVoiceMessageButton + if nextSendGrpInv { + inviteMemberContactButton() + } else if showVoiceMessageButton && composeState.message.isEmpty && !composeState.editing && composeState.liveMessage == nil @@ -119,7 +133,7 @@ struct SendMessageView: View { startVoiceMessageRecording: startVoiceMessageRecording, finishVoiceMessageRecording: finishVoiceMessageRecording, holdingVMR: $holdingVMR, - disabled: composeState.disabled + disabled: composeState.inProgress ) } else { voiceMessageNotAllowedButton() @@ -151,6 +165,24 @@ struct SendMessageView: View { .padding([.top, .trailing], 4) } + private func inviteMemberContactButton() -> some View { + Button { + sendMessage(nil) + } label: { + Image(systemName: "arrow.up.circle.fill") + .resizable() + .foregroundColor(sendButtonColor) + .frame(width: sendButtonSize, height: sendButtonSize) + .opacity(sendButtonOpacity) + } + .disabled( + !composeState.sendEnabled || + composeState.inProgress + ) + .frame(width: 29, height: 29) + .padding([.bottom, .trailing], 4) + } + private func sendMessageButton() -> some View { Button { sendMessage(nil) @@ -159,13 +191,13 @@ struct SendMessageView: View { ? "checkmark.circle.fill" : "arrow.up.circle.fill") .resizable() - .foregroundColor(.accentColor) + .foregroundColor(sendButtonColor) .frame(width: sendButtonSize, height: sendButtonSize) .opacity(sendButtonOpacity) } .disabled( !composeState.sendEnabled || - composeState.disabled || + composeState.inProgress || (!voiceMessageAllowed && composeState.voicePreview) || composeState.endLiveDisabled ) @@ -293,7 +325,7 @@ struct SendMessageView: View { Image(systemName: "mic") .foregroundColor(.secondary) } - .disabled(composeState.disabled) + .disabled(composeState.inProgress) .frame(width: 29, height: 29) .padding([.bottom, .trailing], 4) } @@ -378,7 +410,7 @@ struct SendMessageView: View { Image(systemName: "stop.fill") .foregroundColor(.accentColor) } - .disabled(composeState.disabled) + .disabled(composeState.inProgress) .frame(width: 29, height: 29) .padding([.bottom, .trailing], 4) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 87954def69..3b9ef347e8 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -9,6 +9,8 @@ import SwiftUI import SimpleXChat +let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 + struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @@ -21,6 +23,8 @@ struct GroupChatInfoView: View { @State private var showAddMembersSheet: Bool = false @State private var connectionStats: ConnectionStats? @State private var connectionCode: String? + @State private var sendReceipts = SendReceipts.userDefault(true) + @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var searchText: String = "" @FocusState private var searchFocussed @@ -30,6 +34,7 @@ struct GroupChatInfoView: View { case clearChatAlert case leaveGroupAlert case cantInviteIncognitoAlert + case largeGroupReceiptsDisabled var id: GroupChatInfoViewAlert { get { self } } } @@ -52,6 +57,11 @@ struct GroupChatInfoView: View { addOrEditWelcomeMessage() } groupPreferencesButton($groupInfo) + if members.filter({ $0.memberCurrent }).count <= SMALL_GROUPS_RCPS_MEM_LIMIT { + sendReceiptsOption() + } else { + sendReceiptsOptionDisabled() + } } header: { Text("") } footer: { @@ -115,9 +125,14 @@ struct GroupChatInfoView: View { case .clearChatAlert: return clearChatAlert() case .leaveGroupAlert: return leaveGroupAlert() case .cantInviteIncognitoAlert: return cantInviteIncognitoAlert() + case .largeGroupReceiptsDisabled: return largeGroupReceiptsDisabledAlert() } } .onAppear { + if let currentUser = chatModel.currentUser { + sendReceiptsUserDefault = currentUser.sendRcptsSmallGroups + } + sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault) do { if let link = try apiGetGroupLink(groupInfo.groupId) { (groupLink, groupLinkMemberRole) = link @@ -284,9 +299,9 @@ struct GroupChatInfoView: View { do { try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) await MainActor.run { - chatModel.removeChat(chat.chatInfo.id) - chatModel.chatId = nil dismiss() + chatModel.chatId = nil + chatModel.removeChat(chat.chatInfo.id) } } catch let error { logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)") @@ -328,6 +343,38 @@ struct GroupChatInfoView: View { secondaryButton: .cancel() ) } + + private func sendReceiptsOption() -> some View { + Picker(selection: $sendReceipts) { + ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in + Text(opt.text) + } + } label: { + Label("Send receipts", systemImage: "checkmark.message") + } + .frame(height: 36) + .onChange(of: sendReceipts) { _ in + setSendReceipts() + } + } + + private func setSendReceipts() { + var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults + chatSettings.sendRcpts = sendReceipts.bool() + updateChatSettings(chat, chatSettings: chatSettings) + } + + private func sendReceiptsOptionDisabled() -> some View { + HStack { + Label("Send receipts", systemImage: "checkmark.message") + Spacer() + Text("disabled") + .foregroundStyle(.secondary) + } + .onTapGesture { + alert = .largeGroupReceiptsDisabled + } + } } func groupPreferencesButton(_ groupInfo: Binding<GroupInfo>, _ creatingGroup: Bool = false) -> some View { @@ -356,6 +403,13 @@ func cantInviteIncognitoAlert() -> Alert { ) } +func largeGroupReceiptsDisabledAlert() -> Alert { + Alert( + title: Text("Receipts are disabled"), + message: Text("This group has over \(SMALL_GROUPS_RCPS_MEM_LIMIT) members, delivery receipts are not sent.") + ) +} + struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), groupInfo: GroupInfo.sampleData) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 37e00863fe..5ec14f5be1 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -19,8 +19,10 @@ struct GroupMemberInfoView: View { @State private var connectionCode: String? = nil @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? + @State private var connectToMemberDialog: Bool = false @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @State private var justOpened = true + @State private var progressIndicator = false enum GroupMemberInfoViewAlert: Identifiable { case removeMemberAlert(mem: GroupMember) @@ -38,8 +40,8 @@ struct GroupMemberInfoView: View { case let .changeMemberRoleAlert(_, role): return "changeMemberRoleAlert \(role.rawValue)" case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" - case .connRequestSentAlert: return "connRequestSentAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" + case .connRequestSentAlert: return "connRequestSentAlert" case let .error(title, _): return "error \(title)" case let .other(alert): return "other \(alert)" } @@ -64,161 +66,173 @@ struct GroupMemberInfoView: View { } private func groupMemberInfoView() -> some View { - VStack { - List { - groupMemberInfoHeader(member) - .listRowBackground(Color.clear) + ZStack { + VStack { + List { + groupMemberInfoHeader(member) + .listRowBackground(Color.clear) - if member.memberActive { - Section { - if let contactId = member.memberContactId { - if let chat = knownDirectChat(contactId) { + if member.memberActive { + Section { + if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) { knownDirectChatButton(chat) } else if groupInfo.fullGroupPreferences.directMessages.on { - newDirectChatButton(contactId) + if let contactId = member.memberContactId { + newDirectChatButton(contactId) + } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { + createMemberContactButton() + } } + if let code = connectionCode { verifyCodeButton(code) } + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } } - if let code = connectionCode { verifyCodeButton(code) } - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { - synchronizeConnectionButton() - } -// } else if developerTools { -// synchronizeConnectionButtonForce() -// } } - } - if let contactLink = member.contactLink { - Section { - QRCode(uri: contactLink) - Button { - showShareSheet(items: [contactLink]) - } label: { - Label("Share address", systemImage: "square.and.arrow.up") - } - if let contactId = member.memberContactId { - if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on { + if let contactLink = member.contactLink { + Section { + QRCode(uri: contactLink) + Button { + showShareSheet(items: [contactLink]) + } label: { + Label("Share address", systemImage: "square.and.arrow.up") + } + if let contactId = member.memberContactId { + if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on { + connectViaAddressButton(contactLink) + } + } else { connectViaAddressButton(contactLink) } - } else { - connectViaAddressButton(contactLink) + } header: { + Text("Address") + } footer: { + Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") } - } header: { - Text("Address") - } footer: { - Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") } - } - Section("Member") { - infoRow("Group", groupInfo.displayName) + Section("Member") { + infoRow("Group", groupInfo.displayName) - if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { - Picker("Change role", selection: $newRole) { - ForEach(roles) { role in - Text(role.text) + if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { + Picker("Change role", selection: $newRole) { + ForEach(roles) { role in + Text(role.text) + } } + .frame(height: 36) + } else { + infoRow("Role", member.memberRole.text) + } + + // TODO invited by - need to get contact by contact id + if let conn = member.activeConn { + let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) + infoRow("Connection", connLevelDesc) } - .frame(height: 36) - } else { - infoRow("Role", member.memberRole.text) } - // TODO invited by - need to get contact by contact id - if let conn = member.activeConn { - let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) - infoRow("Connection", connLevelDesc) - } - } - - if let connStats = connectionStats { - Section("Servers") { - // TODO network connection status - Button("Change receiving address") { - alert = .switchAddressAlert - } - .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } - || connStats.ratchetSyncSendProhibited - ) - if connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } { - Button("Abort changing address") { - alert = .abortSwitchAddressAlert + if let connStats = connectionStats { + Section("Servers") { + // TODO network connection status + Button("Change receiving address") { + alert = .switchAddressAlert } .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } || connStats.ratchetSyncSendProhibited ) + if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { + Button("Abort changing address") { + alert = .abortSwitchAddressAlert + } + .disabled( + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + || connStats.ratchetSyncSendProhibited + ) + } + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) } - } - if member.canBeRemoved(groupInfo: groupInfo) { - Section { - removeMemberButton(member) + if member.canBeRemoved(groupInfo: groupInfo) { + Section { + removeMemberButton(member) + } } - } - if developerTools { - Section("For console") { - infoRow("Local name", member.localDisplayName) - infoRow("Database ID", "\(member.groupMemberId)") + if developerTools { + Section("For console") { + infoRow("Local name", member.localDisplayName) + infoRow("Database ID", "\(member.groupMemberId)") + } + } + } + .navigationBarHidden(true) + .onAppear { + if #unavailable(iOS 16) { + // this condition prevents re-setting picker + if !justOpened { return } + } + newRole = member.memberRole + do { + let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) + let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) + member = mem + connectionStats = stats + connectionCode = code + } catch let error { + logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") + } + justOpened = false + } + .onChange(of: newRole) { _ in + if newRole != member.memberRole { + alert = .changeMemberRoleAlert(mem: member, role: newRole) } } } - .navigationBarHidden(true) - .onAppear { - if #unavailable(iOS 16) { - // this condition prevents re-setting picker - if !justOpened { return } - } - newRole = member.memberRole - do { - let (_, stats) = try apiGroupMemberInfo(groupInfo.apiId, member.groupMemberId) - let (mem, code) = member.memberActive ? try apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) : (member, nil) - member = mem - connectionStats = stats - connectionCode = code - } catch let error { - logger.error("apiGroupMemberInfo or apiGetGroupMemberCode error: \(responseError(error))") - } - justOpened = false - } - .onChange(of: newRole) { _ in - if newRole != member.memberRole { - alert = .changeMemberRoleAlert(mem: member, role: newRole) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .alert(item: $alert) { alertItem in + switch(alertItem) { + case let .removeMemberAlert(mem): return removeMemberAlert(mem) + case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem) + case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) + case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) + case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) + case let .connRequestSentAlert(type): return connReqSentAlert(type) + case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .other(alert): return alert } } - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) - .alert(item: $alert) { alertItem in - switch(alertItem) { - case let .removeMemberAlert(mem): return removeMemberAlert(mem) - case let .changeMemberRoleAlert(mem, _): return changeMemberRoleAlert(mem) - case .switchAddressAlert: return switchAddressAlert(switchMemberAddress) - case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) - case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) - case let .connRequestSentAlert(type): return connReqSentAlert(type) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) - case let .other(alert): return alert + + if progressIndicator { + ProgressView().scaleEffect(2) } } } func connectViaAddressButton(_ contactLink: String) -> some View { Button { - connectViaAddress(contactLink) + connectToMemberDialog = true } label: { Label("Connect", systemImage: "link") } + .confirmationDialog("Connect directly", isPresented: $connectToMemberDialog, titleVisibility: .visible) { + Button("Use current profile") { connectViaAddress(incognito: false, contactLink: contactLink) } + Button("Use new incognito profile") { connectViaAddress(incognito: true, contactLink: contactLink) } + } } - func connectViaAddress(_ contactLink: String) { + func connectViaAddress(incognito: Bool, contactLink: String) { Task { - let (connReqType, connectAlert) = await apiConnect_(connReq: contactLink) + let (connReqType, connectAlert) = await apiConnect_(incognito: incognito, connReq: contactLink) if let connReqType = connReqType { alert = .connRequestSentAlert(type: connReqType) } else if let connectAlert = connectAlert { @@ -255,6 +269,33 @@ struct GroupMemberInfoView: View { } } + func createMemberContactButton() -> some View { + Button { + progressIndicator = true + Task { + do { + let memberContact = try await apiCreateMemberContact(groupInfo.apiId, member.groupMemberId) + await MainActor.run { + progressIndicator = false + chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) + dismissAllSheets(animated: true) + chatModel.chatId = memberContact.id + chatModel.setContactNetworkStatus(memberContact, .connected) + } + } catch let error { + logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") + let a = getErrorAlert(error, "Error creating member contact") + await MainActor.run { + progressIndicator = false + alert = .error(title: a.title, error: a.message) + } + } + } + } label: { + Label("Send direct message", systemImage: "message") + } + } + private func groupMemberInfoHeader(_ mem: GroupMember) -> some View { VStack { ProfileImage(imageStr: mem.image, color: Color(uiColor: .tertiarySystemFill)) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 32adf861f3..729556e739 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -130,7 +130,7 @@ struct GroupProfileView: View { let err = responseError(error) saveGroupError = err showSaveErrorAlert = true - logger.error("UserProfile apiUpdateProfile error: \(err)") + logger.error("GroupProfile apiUpdateGroup error: \(err)") } } } diff --git a/apps/ios/Shared/Views/Chat/ScanCodeView.swift b/apps/ios/Shared/Views/Chat/ScanCodeView.swift index d5b6edf906..09861fa50b 100644 --- a/apps/ios/Shared/Views/Chat/ScanCodeView.swift +++ b/apps/ios/Shared/Views/Chat/ScanCodeView.swift @@ -19,7 +19,7 @@ struct ScanCodeView: View { VStack(alignment: .leading) { CodeScannerView(codeTypes: [.qr], completion: processQRCode) .aspectRatio(1, contentMode: .fit) - .border(.gray) + .cornerRadius(12) Text("Scan security code from your contact's app.") .padding(.top) } diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 21269f8435..75e31c26ed 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -64,7 +64,7 @@ struct VerifyCodeView: View { HStack { NavigationLink { ScanCodeView(connectionVerified: $connectionVerified, verify: verify) - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) .navigationTitle("Scan code") } label: { Label("Scan code", systemImage: "qrcode") diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 2521c919a1..e7580530b6 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -49,11 +49,10 @@ struct ChatListNavLink: View { } @ViewBuilder private func contactNavLink(_ contact: Contact) -> some View { - let v = NavLinkPlain( + NavLinkPlain( tag: chat.chatInfo.id, selection: $chatModel.chatId, - label: { ChatPreviewView(chat: chat) }, - disabled: !contact.ready + label: { ChatPreviewView(chat: chat) } ) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() @@ -76,14 +75,6 @@ struct ChatListNavLink: View { .tint(.red) } .frame(height: rowHeights[dynamicTypeSize]) - - if contact.ready { - v - } else { - v.onTapGesture { - AlertManager.shared.showAlert(pendingContactAlert(chat, contact)) - } - } } @ViewBuilder private func groupNavLink(_ groupInfo: GroupInfo) -> some View { @@ -222,9 +213,15 @@ struct ChatListNavLink: View { ContactRequestView(contactRequest: contactRequest, chat: chat) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - Task { await acceptContactRequest(contactRequest) } - } label: { Label("Accept", systemImage: chatModel.incognito ? "theatermasks" : "checkmark") } - .tint(chatModel.incognito ? .indigo : .accentColor) + Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } + } label: { Label("Accept", systemImage: "checkmark") } + .tint(.accentColor) + Button { + Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } + } label: { + Label("Accept incognito", systemImage: "theatermasks") + } + .tint(.indigo) Button { AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest)) } label: { @@ -234,9 +231,10 @@ struct ChatListNavLink: View { } .frame(height: rowHeights[dynamicTypeSize]) .onTapGesture { showContactRequestDialog = true } - .confirmationDialog("Connection request", isPresented: $showContactRequestDialog, titleVisibility: .visible) { - Button(chatModel.incognito ? "Accept incognito" : "Accept contact") { Task { await acceptContactRequest(contactRequest) } } - Button("Reject contact (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } } + .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { + Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } } + Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } } + Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 03dd241087..eb0a5cba68 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -60,8 +60,6 @@ struct ChatListView: View { chatList } } - .onChange(of: chatModel.appOpenUrl) { _ in connectViaUrl() } - .onAppear() { connectViaUrl() } .onDisappear() { withAnimation { userPickerVisible = false } } .refreshable { AlertManager.shared.showAlert(Alert( @@ -108,11 +106,6 @@ struct ChatListView: View { } ToolbarItem(placement: .principal) { HStack(spacing: 4) { - if (chatModel.incognito) { - Image(systemName: "theatermasks") - .foregroundColor(.indigo) - .padding(.trailing, 8) - } Text("Chats") .font(.headline) if chatModel.chats.count > 0 { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 7f2493dc26..3ac8fada74 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -15,6 +15,8 @@ struct ChatPreviewView: View { @Environment(\.colorScheme) var colorScheme var darkGreen = Color(red: 0, green: 0.5, blue: 0) + @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + var body: some View { let cItem = chat.chatItems.last return HStack(spacing: 8) { @@ -41,11 +43,9 @@ struct ChatPreviewView: View { ZStack(alignment: .topTrailing) { chatMessagePreview(cItem) - if case .direct = chat.chatInfo { - chatStatusImage() - .padding(.top, 24) - .frame(maxWidth: .infinity, alignment: .trailing) - } + chatStatusImage() + .padding(.top, 26) + .frame(maxWidth: .infinity, alignment: .trailing) } .padding(.trailing, 8) @@ -59,12 +59,9 @@ struct ChatPreviewView: View { @ViewBuilder private func chatPreviewImageOverlayIcon() -> some View { if case let .group(groupInfo) = chat.chatInfo { switch (groupInfo.membership.memberStatus) { - case .memLeft: - groupInactiveIcon() - case .memRemoved: - groupInactiveIcon() - case .memGroupDeleted: - groupInactiveIcon() + case .memLeft: groupInactiveIcon() + case .memRemoved: groupInactiveIcon() + case .memGroupDeleted: groupInactiveIcon() default: EmptyView() } } else { @@ -74,7 +71,7 @@ struct ChatPreviewView: View { @ViewBuilder private func groupInactiveIcon() -> some View { Image(systemName: "multiply.circle.fill") - .foregroundColor(.secondary) + .foregroundColor(.secondary.opacity(0.65)) .background(Circle().foregroundColor(Color(uiColor: .systemBackground))) } @@ -106,7 +103,7 @@ struct ChatPreviewView: View { .kerning(-2) } - private func chatPreviewLayout(_ text: Text) -> some View { + private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { ZStack(alignment: .topTrailing) { text .lineLimit(2) @@ -114,6 +111,8 @@ struct ChatPreviewView: View { .frame(maxWidth: .infinity, alignment: .topLeading) .padding(.leading, 8) .padding(.trailing, 36) + .privacySensitive(!showChatPreviews && !draft) + .redacted(reason: .privacy) let s = chat.chatStats if s.unreadCount > 0 || s.unreadChat { unreadCountText(s.unreadCount) @@ -175,14 +174,18 @@ struct ChatPreviewView: View { @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { - chatPreviewLayout(messageDraft(draft)) + chatPreviewLayout(messageDraft(draft), draft: true) } else if let cItem = cItem { chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) } else { switch (chat.chatInfo) { case let .direct(contact): if !contact.ready { - chatPreviewInfoText("connecting…") + if contact.nextSendGrpInv { + chatPreviewInfoText("send direct message") + } else { + chatPreviewInfoText("connecting…") + } } case let .group(groupInfo): switch (groupInfo.membership.memberStatus) { @@ -198,10 +201,7 @@ struct ChatPreviewView: View { @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { groupInfo.membership.memberIncognito ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") - : (chatModel.incognito - ? chatPreviewInfoText("join as \(chatModel.currentUser?.profile.displayName ?? "yourself")") - : chatPreviewInfoText("you are invited to group") - ) + : chatPreviewInfoText("you are invited to group") } @ViewBuilder private func chatPreviewInfoText(_ text: LocalizedStringKey) -> some View { @@ -229,7 +229,7 @@ struct ChatPreviewView: View { switch chat.chatInfo { case let .direct(contact): switch (chatModel.contactNetworkStatus(contact)) { - case .connected: EmptyView() + case .connected: incognitoIcon(chat.chatInfo.incognito) case .error: Image(systemName: "exclamationmark.circle") .resizable() @@ -240,11 +240,23 @@ struct ChatPreviewView: View { ProgressView() } default: - EmptyView() + incognitoIcon(chat.chatInfo.incognito) } } } +@ViewBuilder func incognitoIcon(_ incognito: Bool) -> some View { + if incognito { + Image(systemName: "theatermasks") + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .foregroundColor(.secondary) + } else { + EmptyView() + } +} + func unreadCountText(_ n: Int) -> Text { Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "") } @@ -258,20 +270,20 @@ struct ChatPreviewView_Previews: PreviewProvider { )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)] + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))] )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)], + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatStats: ChatStats(unreadCount: 11, minUnreadItemId: 0) )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent, itemDeleted: .deleted(deletedTs: .now))] + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now))] )) ChatPreviewView(chat: Chat( chatInfo: ChatInfo.sampleData.direct, - chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent)], + chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete))], chatStats: ChatStats(unreadCount: 3, minUnreadItemId: 0) )) ChatPreviewView(chat: Chat( diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 7d850719d6..3e42d2f207 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -15,6 +15,7 @@ struct ContactConnectionInfo: View { @State var contactConnection: PendingContactConnection @State private var alert: CCInfoAlert? @State private var localAlias = "" + @State private var showIncognitoSheet = false @FocusState private var aliasTextFieldFocused: Bool enum CCInfoAlert: Identifiable { @@ -31,19 +32,14 @@ struct ContactConnectionInfo: View { var body: some View { NavigationView { - List { + let v = List { Group { - Text(contactConnection.initiated ? "You invited your contact" : "You accepted connection") + Text(contactConnection.initiated ? "You invited a contact" : "You accepted connection") .font(.largeTitle) .bold() - .padding(.bottom, 16) + .padding(.bottom) Text(contactConnectionText(contactConnection)) - .padding(.bottom, 16) - - if let connReqInv = contactConnection.connReqInv { - OneTimeLinkProfileText(contactConnection: contactConnection, connReqInvitation: connReqInv) - } } .listRowBackground(Color.clear) .listRowSeparator(.hidden) @@ -65,10 +61,16 @@ struct ContactConnectionInfo: View { if contactConnection.initiated, let connReqInv = contactConnection.connReqInv { - oneTimeLinkSection(contactConnection: contactConnection, connReqInvitation: connReqInv) + QRCode(uri: connReqInv) + incognitoEnabled() + shareLinkButton(connReqInv) + oneTimeLinkLearnMoreButton() } else { + incognitoEnabled() oneTimeLinkLearnMoreButton() } + } footer: { + sharedProfileInfo(contactConnection.incognito) } Section { @@ -80,6 +82,14 @@ struct ContactConnectionInfo: View { } } } + if #available(iOS 16, *) { + v + } else { + // navigationBarHidden is added conditionally, + // because the view jumps in iOS 17 if this is added, + // and on iOS 16+ it is hidden without it. + v.navigationBarHidden(true) + } } .alert(item: $alert) { _alert in switch _alert { @@ -128,6 +138,30 @@ struct ContactConnectionInfo: View { ) : "You will be connected when your contact's device is online, please wait or check later!" } + + @ViewBuilder private func incognitoEnabled() -> some View { + if contactConnection.incognito { + ZStack(alignment: .leading) { + Image(systemName: "theatermasks.fill") + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + .foregroundColor(Color.indigo) + .font(.system(size: 14)) + HStack(spacing: 6) { + Text("Incognito") + Image(systemName: "info.circle") + .foregroundColor(.accentColor) + .font(.system(size: 14)) + } + .onTapGesture { + showIncognitoSheet = true + } + .padding(.leading, 36) + } + .sheet(isPresented: $showIncognitoSheet) { + IncognitoHelp() + } + } + } } struct ContactConnectionInfo_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift index 1cd50c606d..d21f347881 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift @@ -58,10 +58,14 @@ struct ContactConnectionView: View { } .padding(.bottom, 2) - Text(contactConnection.description) - .frame(alignment: .topLeading) - .padding(.horizontal, 8) - .padding(.bottom, 2) + ZStack(alignment: .topTrailing) { + Text(contactConnection.description) + .frame(maxWidth: .infinity, alignment: .leading) + incognitoIcon(contactConnection.incognito) + .padding(.top, 26) + .frame(maxWidth: .infinity, alignment: .trailing) + } + .padding(.horizontal, 8) Spacer() } diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index c40f672877..c5c062a6ec 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -24,7 +24,7 @@ struct ContactRequestView: View { Text(contactRequest.chatViewName) .font(.title3) .fontWeight(.bold) - .foregroundColor(chatModel.incognito ? .indigo : .accentColor) + .foregroundColor(.accentColor) .padding(.leading, 8) .frame(alignment: .topLeading) Spacer() diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift index 21b968fde9..1b44c23135 100644 --- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift +++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift @@ -133,7 +133,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable { config.filter = .any(of: [.images, .videos]) config.selectionLimit = selectionLimit config.selection = .ordered - //config.preferredAssetRepresentationMode = .current + config.preferredAssetRepresentationMode = .current let controller = PHPickerViewController(configuration: config) controller.delegate = context.coordinator return controller diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 15883f8340..936c6cb3ab 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -8,11 +8,15 @@ import SwiftUI -func showShareSheet(items: [Any]) { +func showShareSheet(items: [Any], completed: (() -> Void)? = nil) { let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first, let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController { let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) + if let completed = completed { + let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() } + activityViewController.completionWithItemsHandler = handler + } presentedViewController.present(activityViewController, animated: true) } } diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index 44de3c4987..31b6b64f32 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -12,38 +12,92 @@ import SimpleXChat struct AddContactView: View { @EnvironmentObject private var chatModel: ChatModel - var contactConnection: PendingContactConnection? = nil + @Binding var contactConnection: PendingContactConnection? var connReqInvitation: String + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false var body: some View { - List { - OneTimeLinkProfileText(contactConnection: contactConnection, connReqInvitation: connReqInvitation) - .listRowBackground(Color.clear) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - - Section("1-time link") { - oneTimeLinkSection(contactConnection: contactConnection, connReqInvitation: connReqInvitation) + VStack { + List { + Section { + if connReqInvitation != "" { + QRCode(uri: connReqInvitation) + } else { + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(2) + .frame(maxWidth: .infinity) + .padding(.vertical) + } + IncognitoToggle(incognitoEnabled: $incognitoDefault) + .disabled(contactConnection == nil) + shareLinkButton(connReqInvitation) + oneTimeLinkLearnMoreButton() + } header: { + Text("1-time link") + } footer: { + sharedProfileInfo(incognitoDefault) + } } } .onAppear { chatModel.connReqInv = connReqInvitation } + .onChange(of: incognitoDefault) { incognito in + Task { + do { + if let contactConn = contactConnection, + let conn = try await apiSetConnectionIncognito(connId: contactConn.pccConnId, incognito: incognito) { + await MainActor.run { + contactConnection = conn + ChatModel.shared.updateContactConnection(conn) + } + } + } catch { + logger.error("apiSetConnectionIncognito error: \(responseError(error))") + } + } + } } } -@ViewBuilder func oneTimeLinkSection(contactConnection: PendingContactConnection? = nil, connReqInvitation: String) -> some View { - if connReqInvitation != "" { - QRCode(uri: connReqInvitation) - } else { - ProgressView() - .progressViewStyle(.circular) - .scaleEffect(2) - .frame(maxWidth: .infinity) - .padding(.vertical) +struct IncognitoToggle: View { + @Binding var incognitoEnabled: Bool + @State private var showIncognitoSheet = false + + var body: some View { + ZStack(alignment: .leading) { + Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks") + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + .foregroundColor(incognitoEnabled ? Color.indigo : .secondary) + .font(.system(size: 14)) + Toggle(isOn: $incognitoEnabled) { + HStack(spacing: 6) { + Text("Incognito") + Image(systemName: "info.circle") + .foregroundColor(.accentColor) + .font(.system(size: 14)) + } + .onTapGesture { + showIncognitoSheet = true + } + } + .padding(.leading, 36) + } + .sheet(isPresented: $showIncognitoSheet) { + IncognitoHelp() + } } - shareLinkButton(connReqInvitation) - oneTimeLinkLearnMoreButton() } -private func shareLinkButton(_ connReqInvitation: String) -> some View { +func sharedProfileInfo(_ incognito: Bool) -> Text { + let name = ChatModel.shared.currentUser?.displayName ?? "" + return Text( + incognito + ? "A new random profile will be shared." + : "Your profile **\(name)** will be shared." + ) +} + +func shareLinkButton(_ connReqInvitation: String) -> some View { Button { showShareSheet(items: [connReqInvitation]) } label: { @@ -65,26 +119,11 @@ func oneTimeLinkLearnMoreButton() -> some View { } } -struct OneTimeLinkProfileText: View { - @EnvironmentObject private var chatModel: ChatModel - var contactConnection: PendingContactConnection? = nil - var connReqInvitation: String - - var body: some View { - HStack { - if (contactConnection?.incognito ?? chatModel.incognito) { - Image(systemName: "theatermasks").foregroundColor(.indigo) - Text("A random profile will be sent to your contact") - } else { - Image(systemName: "info.circle").foregroundColor(.secondary) - Text("Your chat profile will be sent to your contact") - } - } - } -} - struct AddContactView_Previews: PreviewProvider { static var previews: some View { - AddContactView(connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D") + AddContactView( + contactConnection: Binding.constant(PendingContactConnection.getSampleData()), + connReqInvitation: "https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FFe5ICmvrm4wkrr6X1LTMii-lhBqLeB76%23MCowBQYDK2VuAyEAdhZZsHpuaAk3Hh1q0uNb_6hGTpuwBIrsp2z9U2T0oC0%3D&e2e=v%3D1%26x3dh%3DMEIwBQYDK2VvAzkAcz6jJk71InuxA0bOX7OUhddfB8Ov7xwQIlIDeXBRZaOntUU4brU5Y3rBzroZBdQJi0FKdtt_D7I%3D%2CMEIwBQYDK2VvAzkA-hDvk1duBi1hlOr08VWSI-Ou4JNNSQjseY69QyKm7Kgg1zZjbpGfyBqSZ2eqys6xtoV4ZtoQUXQ%3D" + ) } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 247b91a04a..8df37bb560 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -47,21 +47,13 @@ struct AddGroupView: View { .padding(.vertical, 4) Text("The group is fully decentralized – it is visible only to the members.") .padding(.bottom, 4) - if (m.incognito) { - HStack { - Image(systemName: "info.circle").foregroundColor(.orange).font(.footnote) - Spacer().frame(width: 8) - Text("Incognito mode is not supported here - your main profile will be sent to group members").font(.footnote) - } - .padding(.bottom) - } else { - HStack { - Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) - Spacer().frame(width: 8) - Text("Your chat profile will be sent to group members").font(.footnote) - } - .padding(.bottom) + + HStack { + Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) + Spacer().frame(width: 8) + Text("Your chat profile will be sent to group members").font(.footnote) } + .padding(.bottom) ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { diff --git a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift index 71daf88a8c..0b9cfe7a17 100644 --- a/apps/ios/Shared/Views/NewChat/CreateLinkView.swift +++ b/apps/ios/Shared/Views/NewChat/CreateLinkView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat enum CreateLinkTab { case oneTime @@ -24,6 +25,7 @@ struct CreateLinkView: View { @EnvironmentObject var m: ChatModel @State var selection: CreateLinkTab @State var connReqInvitation: String = "" + @State var contactConnection: PendingContactConnection? = nil @State private var creatingConnReq = false var viaNavLink = false @@ -39,7 +41,7 @@ struct CreateLinkView: View { private func createLinkView() -> some View { TabView(selection: $selection) { - AddContactView(connReqInvitation: connReqInvitation) + AddContactView(contactConnection: $contactConnection, connReqInvitation: connReqInvitation) .tabItem { Label( connReqInvitation == "" @@ -56,7 +58,7 @@ struct CreateLinkView: View { .tag(CreateLinkTab.longTerm) } .onChange(of: selection) { _ in - if case .oneTime = selection, connReqInvitation == "" && !creatingConnReq { + if case .oneTime = selection, connReqInvitation == "", contactConnection == nil && !creatingConnReq { createInvitation() } } @@ -69,12 +71,14 @@ struct CreateLinkView: View { private func createInvitation() { creatingConnReq = true Task { - let connReq = await apiAddContact() - await MainActor.run { - if let connReq = connReq { + if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { + await MainActor.run { connReqInvitation = connReq + contactConnection = pcc m.connReqInv = connReq - } else { + } + } else { + await MainActor.run { creatingConnReq = false } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index 3486ab6ef8..a727ad6be0 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -10,13 +10,13 @@ import SwiftUI import SimpleXChat enum NewChatAction: Identifiable { - case createLink(link: String) + case createLink(link: String, connection: PendingContactConnection) case connectViaLink case createGroup var id: String { switch self { - case let .createLink(link): return "createLink \(link)" + case let .createLink(link, _): return "createLink \(link)" case .connectViaLink: return "connectViaLink" case .createGroup: return "createGroup" } @@ -41,8 +41,8 @@ struct NewChatButton: View { } .sheet(item: $actionSheet) { sheet in switch sheet { - case let .createLink(link): - CreateLinkView(selection: .oneTime, connReqInvitation: link) + case let .createLink(link, pcc): + CreateLinkView(selection: .oneTime, connReqInvitation: link, contactConnection: pcc) case .connectViaLink: ConnectViaLinkView() case .createGroup: AddGroupView() } @@ -51,8 +51,8 @@ struct NewChatButton: View { func addContactAction() { Task { - if let connReq = await apiAddContact() { - actionSheet = .createLink(link: connReq) + if let (connReq, pcc) = await apiAddContact(incognito: incognitoGroupDefault.get()) { + actionSheet = .createLink(link: connReq, connection: pcc) } } } @@ -63,9 +63,9 @@ enum ConnReqType: Equatable { case invitation } -func connectViaLink(_ connectionLink: String, _ dismiss: DismissAction? = nil) { +func connectViaLink(_ connectionLink: String, dismiss: DismissAction? = nil, incognito: Bool) { Task { - if let connReqType = await apiConnect(connReq: connectionLink) { + if let connReqType = await apiConnect(incognito: incognito, connReq: connectionLink) { DispatchQueue.main.async { dismiss?() AlertManager.shared.showAlert(connReqSentAlert(connReqType)) @@ -100,12 +100,12 @@ func checkCRDataGroup(_ crData: CReqClientData) -> Bool { return crData.type == "group" && crData.groupLinkId != nil } -func groupLinkAlert(_ connectionLink: String) -> Alert { +func groupLinkAlert(_ connectionLink: String, incognito: Bool) -> Alert { return Alert( title: Text("Connect via group link?"), message: Text("You will join a group this link refers to and connect to its group members."), - primaryButton: .default(Text("Connect")) { - connectViaLink(connectionLink) + primaryButton: .default(Text(incognito ? "Connect incognito" : "Connect")) { + connectViaLink(connectionLink, incognito: incognito) }, secondaryButton: .cancel() ) diff --git a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift index de390ebad9..ec199327ce 100644 --- a/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/PasteToConnectView.swift @@ -7,76 +7,77 @@ // import SwiftUI +import SimpleXChat struct PasteToConnectView: View { - @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction @State private var connectionLink: String = "" + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false + @FocusState private var linkEditorFocused: Bool var body: some View { - ScrollView { - VStack(alignment: .leading) { - Text("Connect via link") - .font(.largeTitle) - .bold() - .fixedSize(horizontal: false, vertical: true) - .padding(.vertical) - Text("Paste the link you received into the box below to connect with your contact.") - .padding(.bottom, 4) - if (chatModel.incognito) { - HStack { - Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) - Spacer().frame(width: 8) - Text("A random profile will be sent to the contact that you received this link from").font(.footnote) + List { + Text("Connect via link") + .font(.largeTitle) + .bold() + .fixedSize(horizontal: false, vertical: true) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .onTapGesture { linkEditorFocused = false } + + Section { + linkEditor() + + Button { + if connectionLink == "" { + connectionLink = UIPasteboard.general.string ?? "" + } else { + connectionLink = "" } - .padding(.bottom) - } else { - HStack { - Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) - Spacer().frame(width: 8) - Text("Your profile will be sent to the contact that you received this link from").font(.footnote) + } label: { + if connectionLink == "" { + settingsRow("doc.plaintext") { Text("Paste") } + } else { + settingsRow("multiply") { Text("Clear") } } - .padding(.bottom) + } + + Button { + connect() + } label: { + settingsRow("link") { Text("Connect") } + } + .disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil) + + IncognitoToggle(incognitoEnabled: $incognitoDefault) + } footer: { + sharedProfileInfo(incognitoDefault) + + Text(String("\n\n")) + + Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") + } + } + } + + private func linkEditor() -> some View { + ZStack { + Group { + if connectionLink.isEmpty { + TextEditor(text: Binding.constant(NSLocalizedString("Paste the link you received to connect with your contact.", comment: "placeholder"))) + .foregroundColor(.secondary) + .disabled(true) } TextEditor(text: $connectionLink) .onSubmit(connect) .textInputAutocapitalization(.never) .disableAutocorrection(true) - .allowsTightening(false) - .frame(height: 180) - .overlay( - RoundedRectangle(cornerRadius: 10) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) - ) - - HStack(spacing: 20) { - if connectionLink == "" { - Button { - connectionLink = UIPasteboard.general.string ?? "" - } label: { - Label("Paste", systemImage: "doc.plaintext") - } - } else { - Button { - connectionLink = "" - } label: { - Label("Clear", systemImage: "multiply") - } - - } - Spacer() - Button(action: connect, label: { - Label("Connect", systemImage: "link") - }) - .disabled(connectionLink == "" || connectionLink.trimmingCharacters(in: .whitespaces).firstIndex(of: " ") != nil) - } - .frame(height: 48) - .padding(.bottom) - - Text("You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.") + .focused($linkEditorFocused) } - .padding() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .allowsTightening(false) + .padding(.horizontal, -5) + .padding(.top, -8) + .frame(height: 180, alignment: .topLeading) + .frame(maxWidth: .infinity, alignment: .leading) } } @@ -85,9 +86,9 @@ struct PasteToConnectView: View { if let crData = parseLinkQueryData(link), checkCRDataGroup(crData) { dismiss() - AlertManager.shared.showAlert(groupLinkAlert(link)) + AlertManager.shared.showAlert(groupLinkAlert(link, incognito: incognitoDefault)) } else { - connectViaLink(link, dismiss) + connectViaLink(link, dismiss: dismiss, incognito: incognitoDefault) } } } diff --git a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift index 2213dff203..01c8a4659e 100644 --- a/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift +++ b/apps/ios/Shared/Views/NewChat/ScanToConnectView.swift @@ -7,11 +7,12 @@ // import SwiftUI +import SimpleXChat import CodeScanner struct ScanToConnectView: View { - @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss: DismissAction + @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false var body: some View { ScrollView { @@ -19,34 +20,35 @@ struct ScanToConnectView: View { Text("Scan QR code") .font(.largeTitle) .bold() + .fixedSize(horizontal: false, vertical: true) .padding(.vertical) - if (chatModel.incognito) { - HStack { - Image(systemName: "theatermasks").foregroundColor(.indigo).font(.footnote) - Spacer().frame(width: 8) - Text("A random profile will be sent to your contact").font(.footnote) - } - .padding(.bottom) - } else { - HStack { - Image(systemName: "info.circle").foregroundColor(.secondary).font(.footnote) - Spacer().frame(width: 8) - Text("Your chat profile will be sent to your contact").font(.footnote) - } - .padding(.bottom) + + CodeScannerView(codeTypes: [.qr], completion: processQRCode) + .aspectRatio(1, contentMode: .fit) + .cornerRadius(12) + + IncognitoToggle(incognitoEnabled: $incognitoDefault) + .padding(.horizontal) + .padding(.vertical, 6) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(uiColor: .systemBackground)) + ) + .padding(.top) + + Group { + sharedProfileInfo(incognitoDefault) + + Text(String("\n\n")) + + Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.") } - ZStack { - CodeScannerView(codeTypes: [.qr], completion: processQRCode) - .aspectRatio(1, contentMode: .fit) - .border(.gray) - } - .padding(.bottom) - Text("If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.") - .padding(.bottom) + .font(.footnote) + .foregroundColor(.secondary) + .padding(.horizontal) } .padding() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) } + .background(Color(.systemGroupedBackground)) } func processQRCode(_ resp: Result<ScanResult, ScanError>) { @@ -55,9 +57,9 @@ struct ScanToConnectView: View { if let crData = parseLinkQueryData(r.string), checkCRDataGroup(crData) { dismiss() - AlertManager.shared.showAlert(groupLinkAlert(r.string)) + AlertManager.shared.showAlert(groupLinkAlert(r.string, incognito: incognitoDefault)) } else { - Task { connectViaLink(r.string, dismiss) } + Task { connectViaLink(r.string, dismiss: dismiss, incognito: incognitoDefault) } } case let .failure(e): logger.error("ConnectContactView.processQRCode QR code error: \(e.localizedDescription)") diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 83ab69278a..966284b0c9 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -251,7 +251,38 @@ private let versionDescriptions: [VersionDescription] = [ description: "- more stable message delivery.\n- a bit better groups.\n- and more!" ), ] - ) + ), + VersionDescription( + version: "v5.3", + post: URL(string: "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html"), + features: [ + FeatureDescription( + icon: "desktopcomputer", + title: "New desktop app!", + description: "Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" + ), + FeatureDescription( + icon: "lock", + title: "Encrypt stored files & media", + description: "App encrypts new local files (except videos)." + ), + FeatureDescription( + icon: "magnifyingglass", + title: "Discover and join groups", + description: "- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." + ), + FeatureDescription( + icon: "theatermasks", + title: "Simplified incognito mode", + description: "Toggle incognito when connecting." + ), + FeatureDescription( + icon: "character", + title: "\(4) new interface languages", + description: "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" + ), + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -321,12 +352,15 @@ struct WhatsNewView: View { private func featureDescription(_ icon: String, _ title: LocalizedStringKey, _ description: LocalizedStringKey) -> some View { VStack(alignment: .leading, spacing: 4) { HStack(alignment: .center, spacing: 4) { - Image(systemName: icon).foregroundColor(.secondary) + Image(systemName: icon) + .symbolRenderingMode(.monochrome) + .foregroundColor(.secondary) .frame(minWidth: 30, alignment: .center) Text(title).font(.title3).bold() } Text(description) .multilineTextAlignment(.leading) + .lineLimit(10) } } diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index be6ccfd3df..94a8937db6 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -22,10 +22,28 @@ struct TerminalView: View { @State var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) @State private var terminalItem: TerminalItem? @State private var scrolled = false + @State private var showing = false var body: some View { if authorized { terminalView() + .onAppear { + if showing { return } + showing = true + Task { + let items = await TerminalItems.shared.items() + await MainActor.run { + chatModel.terminalItems = items + chatModel.showingTerminal = true + } + } + } + .onDisappear { + if terminalItem == nil { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } + } } else { Button(action: runAuth) { Label("Unlock", systemImage: "lock") } .onAppear(perform: runAuth) @@ -118,9 +136,8 @@ struct TerminalView: View { let cmd = ChatCommand.string(composeState.message) if composeState.message.starts(with: "/sql") && (!prefPerformLA || !developerTools) { let resp = ChatResponse.chatCmdError(user_: nil, chatError: ChatError.error(errorType: ChatErrorType.commandError(message: "Failed reading: empty"))) - DispatchQueue.main.async { - ChatModel.shared.addTerminalItem(.cmd(.now, cmd)) - ChatModel.shared.addTerminalItem(.resp(.now, resp)) + Task { + await TerminalItems.shared.addCommand(.now, cmd, resp) } } else { DispatchQueue.global().async { diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift index 554daaebb1..8e8885b518 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -53,7 +53,7 @@ struct AdvancedNetworkSettings: View { timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [5_000000, 7_500000, 10_000000, 15_000000, 20_000000, 30_000000, 45_000000], label: secondsLabel) timeoutSettingPicker("Protocol timeout", selection: $netCfg.tcpTimeout, values: [3_000000, 5_000000, 7_000000, 10_000000, 15_000000, 20_000000, 30_000000], label: secondsLabel) - timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [10_000, 20_000, 40_000, 75_000, 100_000], label: secondsLabel) + timeoutSettingPicker("Protocol timeout per KB", selection: $netCfg.tcpTimeoutPerKb, values: [15_000, 30_000, 60_000, 90_000, 120_000], label: secondsLabel) timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index 3652dec054..20dadb7954 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -18,10 +18,9 @@ struct IncognitoHelp: View { ScrollView { VStack(alignment: .leading) { Group { - Text("Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.") + Text("Incognito mode protects your privacy by using a new random profile for each contact.") Text("It allows having many anonymous connections without any shared data between them in a single chat profile.") Text("When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.") - Text("To find the profile used for an incognito connection, tap the contact or group name on top of the chat.") } .padding(.bottom) } diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index cf8978498b..960afb6d38 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -71,9 +71,10 @@ struct PreferencesView: View { do { var p = fromLocalProfile(profile) p.preferences = fullPreferencesToPreferences(preferences) - if let newProfile = try await apiUpdateProfile(profile: p) { + if let (newProfile, updatedContacts) = try await apiUpdateProfile(profile: p) { await MainActor.run { chatModel.updateCurrentUser(newProfile, preferences) + updatedContacts.forEach(chatModel.updateContact) currentPreferences = preferences } } diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 789a5330e5..34b6f147bd 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -13,6 +13,9 @@ struct PrivacySettings: View { @EnvironmentObject var m: ChatModel @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = 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 @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @@ -21,6 +24,10 @@ struct PrivacySettings: View { @State private var contactReceiptsReset = false @State private var contactReceiptsOverrides = 0 @State private var contactReceiptsDialogue = false + @State private var groupReceipts = false + @State private var groupReceiptsReset = false + @State private var groupReceiptsOverrides = 0 + @State private var groupReceiptsDialogue = false @State private var alert: PrivacySettingsViewAlert? enum PrivacySettingsViewAlert: Identifiable { @@ -57,6 +64,9 @@ struct PrivacySettings: View { } Section { + settingsRow("lock.doc") { + Toggle("Encrypt local files", isOn: $encryptLocalFiles) + } settingsRow("photo") { Toggle("Auto-accept images", isOn: $autoAcceptImages) .onChange(of: autoAcceptImages) { @@ -66,6 +76,18 @@ struct PrivacySettings: View { settingsRow("network") { Toggle("Send link previews", isOn: $useLinkPreviews) } + settingsRow("message") { + Toggle("Show last messages", isOn: $showChatPreviews) + } + settingsRow("rectangle.and.pencil.and.ellipsis") { + Toggle("Message draft", isOn: $saveLastDraft) + } + .onChange(of: saveLastDraft) { saveDraft in + if !saveDraft { + m.draft = nil + m.draftChatId = nil + } + } settingsRow("link") { Picker("SimpleX links", selection: $simplexLinkMode) { ForEach(SimpleXLinkMode.values) { mode in @@ -89,15 +111,15 @@ struct PrivacySettings: View { settingsRow("person") { Toggle("Contacts", isOn: $contactReceipts) } -// settingsRow("person.2") { -// Toggle("Small groups (max 20)", isOn: Binding.constant(false)) -// } + settingsRow("person.2") { + Toggle("Small groups (max 20)", isOn: $groupReceipts) + } } header: { Text("Send delivery receipts to") } footer: { VStack(alignment: .leading) { Text("These settings are for your current profile **\(ChatModel.shared.currentUser?.displayName ?? "")**.") - Text("They can be overridden in contact settings") + Text("They can be overridden in contact and group settings.") } .frame(maxWidth: .infinity, alignment: .leading) } @@ -113,19 +135,44 @@ struct PrivacySettings: View { contactReceipts.toggle() } } + .confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) { + Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsGroups(groupReceipts, clearOverrides: false) + } + Button(groupReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsGroups(groupReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + groupReceiptsReset = true + groupReceipts.toggle() + } + } } } - .onChange(of: contactReceipts) { _ in // sometimes there is race with onAppear + .onChange(of: contactReceipts) { _ in if contactReceiptsReset { contactReceiptsReset = false } else { setOrAskSendReceiptsContacts(contactReceipts) } } + .onChange(of: groupReceipts) { _ in + if groupReceiptsReset { + groupReceiptsReset = false + } else { + setOrAskSendReceiptsGroups(groupReceipts) + } + } .onAppear { - if let u = m.currentUser, contactReceipts != u.sendRcptsContacts { - contactReceiptsReset = true - contactReceipts = u.sendRcptsContacts + if let u = m.currentUser { + if contactReceipts != u.sendRcptsContacts { + contactReceiptsReset = true + contactReceipts = u.sendRcptsContacts + } + if groupReceipts != u.sendRcptsSmallGroups { + groupReceiptsReset = true + groupReceipts = u.sendRcptsSmallGroups + } } } .alert(item: $alert) { alert in @@ -184,6 +231,54 @@ struct PrivacySettings: View { } } + private func setOrAskSendReceiptsGroups(_ enable: Bool) { + groupReceiptsOverrides = m.chats.reduce(0) { count, chat in + let sendRcpts = chat.chatInfo.groupInfo?.chatSettings.sendRcpts + return count + (sendRcpts == nil || sendRcpts == enable ? 0 : 1) + } + if groupReceiptsOverrides == 0 { + setSendReceiptsGroups(enable, clearOverrides: false) + } else { + groupReceiptsDialogue = true + } + } + + private var groupReceiptsDialogTitle: LocalizedStringKey { + groupReceipts + ? "Sending receipts is disabled for \(groupReceiptsOverrides) groups" + : "Sending receipts is enabled for \(groupReceiptsOverrides) groups" + } + + private func setSendReceiptsGroups(_ enable: Bool, clearOverrides: Bool) { + Task { + do { + if let currentUser = m.currentUser { + let userMsgReceiptSettings = UserMsgReceiptSettings(enable: enable, clearOverrides: clearOverrides) + try await apiSetUserGroupReceipts(currentUser.userId, userMsgReceiptSettings: userMsgReceiptSettings) + privacyDeliveryReceiptsSet.set(true) + await MainActor.run { + var updatedUser = currentUser + updatedUser.sendRcptsSmallGroups = enable + m.updateUser(updatedUser) + if clearOverrides { + m.chats.forEach { chat in + if var groupInfo = chat.chatInfo.groupInfo { + let sendRcpts = groupInfo.chatSettings.sendRcpts + if sendRcpts != nil && sendRcpts != enable { + groupInfo.chatSettings.sendRcpts = nil + m.updateGroup(groupInfo) + } + } + } + } + } + } + } catch let error { + alert = .error(title: "Error setting delivery receipts!", error: "Error: \(responseError(error))") + } + } + } + private func simplexLockRow(_ value: LocalizedStringKey) -> some View { HStack { Text("SimpleX Lock") diff --git a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift b/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift index c0ad4e3e18..ffdbd1b07e 100644 --- a/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift +++ b/apps/ios/Shared/Views/UserSettings/ScanProtocolServer.swift @@ -21,11 +21,10 @@ struct ScanProtocolServer: View { .font(.largeTitle) .bold() .padding(.vertical) - ZStack { - CodeScannerView(codeTypes: [.qr], completion: processQRCode) - .aspectRatio(1, contentMode: .fit) - .border(.gray) - } + CodeScannerView(codeTypes: [.qr], completion: processQRCode) + .aspectRatio(1, contentMode: .fit) + .cornerRadius(12) + .padding(.top) } .padding() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 7ca18692a8..d6681a51cd 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -30,6 +30,8 @@ let DEFAULT_CALL_KIT_CALLS_IN_RECENTS = "callKitCallsInRecents" let DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" let DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" +let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" +let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls" @@ -65,6 +67,8 @@ let appDefaults: [String: Any] = [ DEFAULT_PRIVACY_ACCEPT_IMAGES: true, DEFAULT_PRIVACY_LINK_PREVIEWS: true, DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, + DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, + DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, DEFAULT_EXPERIMENTAL_CALLS: false, @@ -131,7 +135,6 @@ struct SettingsView: View { @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var sceneDelegate: SceneDelegate @Binding var showSettings: Bool - @State private var settingsSheet: SettingsSheet? var body: some View { ZStack { @@ -161,8 +164,6 @@ struct SettingsView: View { settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") } } - incognitoRow() - NavigationLink { UserAddressView(shareViaProfile: chatModel.currentUser!.addressShared) .navigationTitle("SimpleX address") @@ -298,38 +299,9 @@ struct SettingsView: View { } .navigationTitle("Your settings") } - .sheet(item: $settingsSheet) { sheet in - switch sheet { - case .incognitoInfo: IncognitoHelp() - } - } - } - - @ViewBuilder private func incognitoRow() -> some View { - ZStack(alignment: .leading) { - Image(systemName: chatModel.incognito ? "theatermasks.fill" : "theatermasks") - .frame(maxWidth: 24, maxHeight: 24, alignment: .center) - .foregroundColor(chatModel.incognito ? Color.indigo : .secondary) - Toggle(isOn: $chatModel.incognito) { - HStack(spacing: 6) { - Text("Incognito") - Image(systemName: "info.circle") - .foregroundColor(.accentColor) - .font(.system(size: 14)) - } - .onTapGesture { - settingsSheet = .incognitoInfo - } - } - .onChange(of: chatModel.incognito) { incognito in - incognitoGroupDefault.set(incognito) - do { - try apiSetIncognito(incognito: incognito) - } catch { - logger.error("apiSetIncognito: cannot set incognito \(responseError(error))") - } - } - .padding(.leading, indent) + .onDisappear { + chatModel.showingTerminal = false + chatModel.terminalItems = [] } } @@ -351,12 +323,6 @@ struct SettingsView: View { } } - private enum SettingsSheet: Identifiable { - case incognitoInfo - - var id: SettingsSheet { get { self } } - } - private enum NotificationAlert { case enable case error(LocalizedStringKey, String) diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 134eeb969b..86bf0048b8 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -190,7 +190,7 @@ struct UserAddressView: View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { Section { - QRCode(uri: userAddress.connReqContact) + MutableQRCode(uri: Binding.constant(userAddress.connReqContact)) shareQRCodeButton(userAddress) if MFMailComposeViewController.canSendMail() { shareViaEmailButton(userAddress) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index 9ea6fe2a7f..f38dc593a6 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -144,7 +144,7 @@ struct UserProfile: View { func saveProfile() { Task { do { - if let newProfile = try await apiUpdateProfile(profile: profile) { + if let (newProfile, _) = try await apiUpdateProfile(profile: profile) { DispatchQueue.main.async { chatModel.updateCurrentUser(newProfile) profile = newProfile diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index 99da18e12c..e0477899be 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -3630,6 +3630,51 @@ SimpleX servers cannot see your profile.</source> <source>\~strike~</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ servers" xml:space="preserve" approved="no"> + <source>%@ servers</source> + <target state="translated">%@ الخوادم</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <source>%@ (current)</source> + <target state="translated">%@ (الحالي)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <source>%@ (current):</source> + <target state="translated">%@ (الحالي):</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve" approved="no"> + <source>%@:</source> + <target state="needs-translation">%@:</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%1$@ في %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="needs-translation"># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## السجل</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">## ردًا على</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ و %@ متصل</target> + <note>No comment provided by engineer.</note> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ar" datatype="plaintext"> diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..59aaf6069d --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "bg" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 9f1d8cdcaa..2bf06561a2 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2,2107 +2,2775 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="bg" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> <source> </source> + <target> +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" " xml:space="preserve"> <source> </source> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" " xml:space="preserve"> <source> </source> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" " xml:space="preserve"> <source> </source> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" (" xml:space="preserve"> <source> (</source> + <target> (</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> + <target> (може да се копира)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="!1 colored!" xml:space="preserve"> <source>!1 colored!</source> + <target>!1 цветно!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## История</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## В отговор на</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> + <target>#тайно#</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@" xml:space="preserve"> <source>%@</source> + <target>%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ %@" xml:space="preserve"> <source>%@ %@</source> + <target>%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ (current)" xml:space="preserve"> <source>%@ (current)</source> + <target>%@ (текущ)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ (current):" xml:space="preserve"> <source>%@ (current):</source> + <target>%@ (текущ):</target> <note>copied message info</note> </trans-unit> <trans-unit id="%@ / %@" xml:space="preserve"> <source>%@ / %@</source> + <target>%@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ и %@ са свързани</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> + <target>%1$@ в %2$@:</target> <note>copied message info, <sender> at <time></note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> + <target>%@ е свързан!</target> <note>notification title</note> </trans-unit> <trans-unit id="%@ is not verified" xml:space="preserve"> <source>%@ is not verified</source> + <target>%@ не е потвърдено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ is verified" xml:space="preserve"> <source>%@ is verified</source> + <target>%@ е потвърдено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> + <target>%@ сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> + <target>%@ иска да се свърже!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ и %lld други членове са свързани</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> + <target>%@:</target> <note>copied message info</note> </trans-unit> <trans-unit id="%d days" xml:space="preserve"> <source>%d days</source> + <target>%d дни</target> <note>time interval</note> </trans-unit> <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> + <target>%d часа</target> <note>time interval</note> </trans-unit> <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> + <target>%d мин.</target> <note>time interval</note> </trans-unit> <trans-unit id="%d months" xml:space="preserve"> <source>%d months</source> + <target>%d месеца</target> <note>time interval</note> </trans-unit> <trans-unit id="%d sec" xml:space="preserve"> <source>%d sec</source> + <target>%d сек.</target> <note>time interval</note> </trans-unit> <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> + <target>%d пропуснато(и) съобщение(я)</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="%d weeks" xml:space="preserve"> <source>%d weeks</source> + <target>%d седмици</target> <note>time interval</note> </trans-unit> <trans-unit id="%lld" xml:space="preserve"> <source>%lld</source> + <target>%lld</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld %@" xml:space="preserve"> <source>%lld %@</source> + <target>%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld contact(s) selected" xml:space="preserve"> <source>%lld contact(s) selected</source> + <target>%lld избран(и) контакт(а)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> <source>%lld file(s) with total size of %@</source> + <target>%lld файл(а) с общ размер от %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld members" xml:space="preserve"> <source>%lld members</source> + <target>%lld членове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld minutes" xml:space="preserve"> <source>%lld minutes</source> + <target>%lld минути</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld нови езици на интерфейса</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> + <target>%lld секунда(и)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> + <target>%lld секунди</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldd" xml:space="preserve"> <source>%lldd</source> + <target>%lldд</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldh" xml:space="preserve"> <source>%lldh</source> + <target>%lldч</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldk" xml:space="preserve"> <source>%lldk</source> + <target>%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldm" xml:space="preserve"> <source>%lldm</source> + <target>%lldм</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldmth" xml:space="preserve"> <source>%lldmth</source> + <target>%lldмесц.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%llds" xml:space="preserve"> <source>%llds</source> + <target>%lldс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%lldw" xml:space="preserve"> <source>%lldw</source> + <target>%lldсед.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> <source>%u messages failed to decrypt.</source> + <target>%u съобщения не успяха да се декриптират.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%u messages skipped." xml:space="preserve"> <source>%u messages skipped.</source> + <target>%u пропуснати съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="(" xml:space="preserve"> <source>(</source> + <target>(</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=")" xml:space="preserve"> <source>)</source> + <target>)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> + <target>**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> + <target>**Създай линк / QR код**, който вашият контакт да използва.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <target>**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <target>**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> + <target>**Поставете получения линк** или го отворете в браузъра и докоснете **Отваряне в мобилно приложение**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> + <target>**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <target>**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> <source>**Scan QR code**: to connect to your contact in person or via video call.</source> + <target>**Сканирай QR код**: за да се свържете с вашия контакт лично или чрез видеообаждане.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> + <target>**Внимание**: Незабавните push известия изискват парола, запазена в Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> + <target>**e2e криптиран**аудио разговор</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> <source>**e2e encrypted** video call</source> + <target>**e2e криптирано** видео разговор</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="*bold*" xml:space="preserve"> <source>\*bold*</source> + <target>\*удебелен*</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=", " xml:space="preserve"> <source>, </source> + <target>, </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjd LW3%23%2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)! +- потвърждениe за доставка (до 20 члена). +- по-бързо и по-стабилно.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target>- по-стабилна доставка на съобщения. +- малко по-добри групи. +- и още!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> <source>- voice messages up to 5 minutes. - custom time to disappear. - editing history.</source> + <target>- гласови съобщения до 5 минути. +- персонализирано време за изчезване. +- история на редактиране.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="." xml:space="preserve"> <source>.</source> + <target>.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="0s" xml:space="preserve"> <source>0s</source> + <target>0s</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> + <target>1 ден</target> <note>time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> + <target>1 час</target> <note>time interval</note> </trans-unit> <trans-unit id="1 minute" xml:space="preserve"> <source>1 minute</source> + <target>1 минута</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> + <target>1 месец</target> <note>time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> + <target>1 седмица</target> <note>time interval</note> </trans-unit> <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> + <target>Еднократен линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> + <target>5 минути</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="6" xml:space="preserve"> <source>6</source> + <target>6</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="30 seconds" xml:space="preserve"> <source>30 seconds</source> + <target>30 секунди</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=": " xml:space="preserve"> <source>: </source> + <target>: </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> + <target><p>Здравейте!</p> +<p><a href="%@">Свържете се с мен чрез SimpleX Chat</a></p></target> <note>email text</note> </trans-unit> + <trans-unit id="A few more things" xml:space="preserve"> + <source>A few more things</source> + <target>Още няколко неща</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="A new contact" xml:space="preserve"> <source>A new contact</source> + <target>Нов контакт</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Нов автоматично генериран профил ще бъде споделен.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> + <target>Ще се използва отделна TCP връзка **за всеки чатпрофил, който имате в приложението**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> + <target>Ще се използва отделна TCP връзка **за всеки контакт и член на групата**. +**Моля, обърнете внимание**: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort" xml:space="preserve"> <source>Abort</source> + <target>Откажи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address" xml:space="preserve"> <source>Abort changing address</source> + <target>Откажи смяна на адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address?" xml:space="preserve"> <source>Abort changing address?</source> + <target>Откажи смяна на адрес?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX" xml:space="preserve"> <source>About SimpleX</source> + <target>За SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> + <target>За SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX address" xml:space="preserve"> <source>About SimpleX address</source> + <target>Повече за SimpleX адреса</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accent color" xml:space="preserve"> <source>Accent color</source> + <target>Основен цвят</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> + <target>Приеми</target> <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Приемане на заявка за връзка?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> <source>Accept contact request from %@?</source> + <target>Приемане на заявка за контакт от %@?</target> <note>notification body</note> </trans-unit> <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> - <note>No comment provided by engineer.</note> + <target>Приеми инкогнито</target> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target>Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add preset servers" xml:space="preserve"> <source>Add preset servers</source> + <target>Добави предварително зададени сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> <source>Add profile</source> + <target>Добави профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> + <target>Добави сървъри чрез сканиране на QR кодове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add server…" xml:space="preserve"> <source>Add server…</source> + <target>Добави сървър…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> <source>Add to another device</source> + <target>Добави към друго устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> + <target>Добави съобщение при посрещане</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Address" xml:space="preserve"> <source>Address</source> + <target>Адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> <source>Address change will be aborted. Old receiving address will be used.</source> + <target>Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> + <target>Админите могат да създадат линкове за присъединяване към групи.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Advanced network settings" xml:space="preserve"> <source>Advanced network settings</source> + <target>Разширени мрежови настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> + <target>Всички данни от приложението бяха изтрити.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>All chats and messages will be deleted - this cannot be undone!</source> + <target>Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> + <target>Всички данни се изтриват при въвеждане.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> + <target>Всички членове на групата ще останат свързани.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target>Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> + <target>Всички ваши контакти ще останат свързани.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target>Всички ваши контакти ще останат свързани. Актуализацията на профила ще бъде изпратена до вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> + <target>Позволи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> <source>Allow calls only if your contact allows them.</source> + <target>Позволи обаждания само ако вашият контакт ги разрешава.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> + <target>Позволи изчезващи съобщения само ако вашият контакт ги разрешава.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> + <target>Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> <source>Allow message reactions only if your contact allows them.</source> + <target>Позволи реакции на съобщения само ако вашият контакт ги разрешава.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions." xml:space="preserve"> <source>Allow message reactions.</source> + <target>Позволи реакции на съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> <source>Allow sending direct messages to members.</source> + <target>Позволи изпращането на лични съобщения до членовете.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> <source>Allow sending disappearing messages.</source> + <target>Разреши изпращането на изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> <source>Allow to irreversibly delete sent messages.</source> + <target>Позволи необратимо изтриване на изпратените съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> <source>Allow to send files and media.</source> + <target>Позволи изпращане на файлове и медия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send voice messages." xml:space="preserve"> <source>Allow to send voice messages.</source> + <target>Позволи изпращане на гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> <source>Allow voice messages only if your contact allows them.</source> + <target>Позволи гласови съобщения само ако вашият контакт ги разрешава.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages?" xml:space="preserve"> <source>Allow voice messages?</source> + <target>Позволи гласови съобщения?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> <source>Allow your contacts adding message reactions.</source> + <target>Позволи на вашите контакти да добавят реакции към съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to call you." xml:space="preserve"> <source>Allow your contacts to call you.</source> + <target>Позволи на вашите контакти да ви се обаждат.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> <source>Allow your contacts to irreversibly delete sent messages.</source> + <target>Позволи на вашите контакти да изтриват необратимо изпратените съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> <source>Allow your contacts to send disappearing messages.</source> + <target>Позволи на вашите контакти да изпращат изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> <source>Allow your contacts to send voice messages.</source> + <target>Позволи на вашите контакти да изпращат гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already connected?" xml:space="preserve"> <source>Already connected?</source> + <target>Вече сте свързани?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> + <target>Винаги използвай реле</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target>Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> + <target>Отговор на повикване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> + <target>Компилация на приложението: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>Приложението криптира нови локални файлове (с изключение на видеоклипове).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> + <target>Икона на приложението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App passcode" xml:space="preserve"> <source>App passcode</source> + <target>Код за достъп до приложението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> <source>App passcode is replaced with self-destruct passcode.</source> + <target>Кода за достъп до приложение се заменя с код за самоунищожение.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> + <target>Версия на приложението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App version: v%@" xml:space="preserve"> <source>App version: v%@</source> + <target>Версия на приложението: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Appearance" xml:space="preserve"> <source>Appearance</source> + <target>Изглед</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> + <target>Прикачи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio & video calls" xml:space="preserve"> <source>Audio & video calls</source> + <target>Аудио и видео разговори</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio and video calls" xml:space="preserve"> <source>Audio and video calls</source> + <target>Аудио и видео разговори</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio/video calls" xml:space="preserve"> <source>Audio/video calls</source> + <target>Аудио/видео разговори</target> <note>chat feature</note> </trans-unit> <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> <source>Audio/video calls are prohibited.</source> + <target>Аудио/видео разговорите са забранени.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication cancelled" xml:space="preserve"> <source>Authentication cancelled</source> + <target>Идентификацията е отменена</target> <note>PIN entry</note> </trans-unit> <trans-unit id="Authentication failed" xml:space="preserve"> <source>Authentication failed</source> + <target>Неуспешна идентификация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target>Изисква се идентификацията, преди да се осъществи обаждането, но може да пропуснете повиквания.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication unavailable" xml:space="preserve"> <source>Authentication unavailable</source> + <target>Идентификацията е недостъпна</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept" xml:space="preserve"> <source>Auto-accept</source> + <target>Автоматично приемане</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept contact requests" xml:space="preserve"> <source>Auto-accept contact requests</source> + <target>Автоматично приемане на заявки за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept images" xml:space="preserve"> <source>Auto-accept images</source> + <target>Автоматично приемане на изображения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> + <target>Назад</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> <source>Bad message ID</source> + <target>Лошо ID на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message hash" xml:space="preserve"> <source>Bad message hash</source> + <target>Лош хеш на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> + <target>По-добри съобщения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> + <target>И вие, и вашият контакт можете да добавяте реакции към съобщението.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> <source>Both you and your contact can irreversibly delete sent messages.</source> + <target>И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> <source>Both you and your contact can make calls.</source> + <target>И вие, и вашият контакт можете да осъществявате обаждания.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> <source>Both you and your contact can send disappearing messages.</source> + <target>И вие, и вашият контакт можете да изпращате изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> <source>Both you and your contact can send voice messages.</source> + <target>И вие, и вашият контакт можете да изпращате гласови съобщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Български, финландски, тайландски и украински - благодарение на потребителите и [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target>Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> + <target>Разговорът вече приключи!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Calls" xml:space="preserve"> <source>Calls</source> + <target>Обаждания</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't delete user profile!" xml:space="preserve"> <source>Can't delete user profile!</source> + <target>Потребителският профил не може да се изтрие!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> <source>Can't invite contact!</source> + <target>Не може да покани контакта!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contacts!" xml:space="preserve"> <source>Can't invite contacts!</source> + <target>Не може да поканят контактите!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> + <target>Отказ</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> <source>Cannot access keychain to save database password</source> + <target>Няма достъп до Keychain за запазване на паролата за базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> + <target>Файлът не може да бъде получен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> <source>Change</source> + <target>Промени</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> + <target>Промяна на паролата на базата данни?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change lock mode" xml:space="preserve"> <source>Change lock mode</source> + <target>Промяна на режима на заключване</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Change member role?" xml:space="preserve"> <source>Change member role?</source> + <target>Промяна на ролята на члена?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change passcode" xml:space="preserve"> <source>Change passcode</source> + <target>Промени kодa за достъп</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Change receiving address" xml:space="preserve"> <source>Change receiving address</source> + <target>Промени адреса за получаване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change receiving address?" xml:space="preserve"> <source>Change receiving address?</source> + <target>Промени адреса за получаване?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change role" xml:space="preserve"> <source>Change role</source> + <target>Промени ролята</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change self-destruct mode" xml:space="preserve"> <source>Change self-destruct mode</source> + <target>Промени режима на самоунищожение</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Change self-destruct passcode" xml:space="preserve"> <source>Change self-destruct passcode</source> + <target>Промени кода за достъп за самоунищожение</target> <note>authentication reason set passcode view</note> </trans-unit> <trans-unit id="Chat archive" xml:space="preserve"> <source>Chat archive</source> + <target>Архив на чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> <source>Chat console</source> + <target>Конзола</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database" xml:space="preserve"> <source>Chat database</source> + <target>База данни за чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database deleted" xml:space="preserve"> <source>Chat database deleted</source> + <target>Базата данни на чата е изтрита</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> + <target>Базата данни на чат е импортирана</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is running" xml:space="preserve"> <source>Chat is running</source> + <target>Чатът работи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is stopped" xml:space="preserve"> <source>Chat is stopped</source> + <target>Чатът е спрян</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> + <target>Чат настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> + <target>Чатове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> + <target>Проверете адреса на сървъра и опитайте отново.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> + <target>Китайски и Испански интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> + <target>Избери файл</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Choose from library" xml:space="preserve"> <source>Choose from library</source> + <target>Избери от библиотеката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> + <target>Изчисти</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> + <target>Изчисти разговора</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear conversation?" xml:space="preserve"> <source>Clear conversation?</source> + <target>Изчисти разговора?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> + <target>Изчисти проверката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Colors" xml:space="preserve"> <source>Colors</source> + <target>Цветове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> + <target>Сравни файл</target> <note>server test step</note> </trans-unit> <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> <source>Compare security codes with your contacts.</source> + <target>Сравнете кодовете за сигурност с вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> + <target>Конфигурирай ICE сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> + <target>Потвърди</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm Passcode" xml:space="preserve"> <source>Confirm Passcode</source> + <target>Потвърди kодa за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> + <target>Потвърди актуализаациите на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> + <target>Потвърди новата парола…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm password" xml:space="preserve"> <source>Confirm password</source> + <target>Потвърди парола</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> + <target>Свързване</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Свързване директно</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Свързване инкогнито</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Свързване чрез линк на контакта</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> <source>Connect via group link?</source> + <target>Свързване чрез групов линк?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> <source>Connect via link</source> + <target>Свърване чрез линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link / QR code" xml:space="preserve"> <source>Connect via link / QR code</source> + <target>Свърване чрез линк/QR код</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Свързване чрез еднократен линк за връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> + <target>Свързване със сървъра…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> <source>Connecting to server… (error: %@)</source> + <target>Свързване със сървър…(грешка: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> <source>Connection</source> + <target>Връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> + <target>Грешка при свързване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection error (AUTH)" xml:space="preserve"> <source>Connection error (AUTH)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> + <target>Грешка при свързване (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> + <target>Заявката за връзка е изпратена!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> <source>Connection timeout</source> + <target>Времето на изчакване за установяване на връзката изтече</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> + <target>Контактът позволява</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact already exists" xml:space="preserve"> <source>Contact already exists</source> + <target>Контактът вече съществува</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>Contact and all messages will be deleted - this cannot be undone!</source> + <target>Контактът и всички съобщения ще бъдат изтрити - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> + <target>Контактът е скрит:</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is connected" xml:space="preserve"> <source>Contact is connected</source> + <target>Контактът е свързан</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is not connected yet!" xml:space="preserve"> <source>Contact is not connected yet!</source> + <target>Контактът все още не е свързан!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> <source>Contact name</source> + <target>Име на контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact preferences" xml:space="preserve"> <source>Contact preferences</source> + <target>Настройки за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> + <target>Контакти</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target>Контактите могат да маркират съобщения за изтриване; ще можете да ги разглеждате.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> + <target>Продължи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> + <target>Копирай</target> <note>chat item action</note> </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> + <target>Версия на ядрото: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> <source>Create</source> + <target>Създай</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> + <target>Създай SimpleX адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <source>Create an address to let people connect with you.</source> + <target>Създайте адрес, за да позволите на хората да се свързват с вас.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> + <target>Създай файл</target> <note>server test step</note> </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> <source>Create group link</source> + <target>Създай групов линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create link" xml:space="preserve"> <source>Create link</source> + <target>Създай линк</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Създайте нов профил в [настолното приложение](https://simplex.chat/downloads/). 💻</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> + <target>Създай линк за еднократна покана</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> <source>Create queue</source> + <target>Създай опашка</target> <note>server test step</note> </trans-unit> <trans-unit id="Create secret group" xml:space="preserve"> <source>Create secret group</source> + <target>Създай тайна група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create your profile" xml:space="preserve"> <source>Create your profile</source> + <target>Създай своя профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Created on %@" xml:space="preserve"> <source>Created on %@</source> + <target>Създаден на %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current Passcode" xml:space="preserve"> <source>Current Passcode</source> + <target>Текущ kод за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> + <target>Текуща парола…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> + <target>В момента максималният поддържан размер на файла е %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Custom time" xml:space="preserve"> <source>Custom time</source> + <target>Персонализирано време</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> + <target>Тъмна</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> + <target>ID в базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database ID: %d" xml:space="preserve"> <source>Database ID: %d</source> + <target>ID в базата данни: %d</target> <note>copied message info</note> </trans-unit> <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> <source>Database IDs and Transport isolation option.</source> + <target>Идентификатори в базата данни и опция за изолация на транспорта.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database downgrade" xml:space="preserve"> <source>Database downgrade</source> + <target>Понижаване на версията на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encrypted!" xml:space="preserve"> <source>Database encrypted!</source> + <target>Базата данни е криптирана!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> <source>Database encryption passphrase will be updated and stored in the keychain. </source> + <target>Паролата за криптиране на базата данни ще бъде актуализирана и съхранена в Keychain. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> <source>Database encryption passphrase will be updated. </source> + <target>Паролата за криптиране на базата данни ще бъде актуализирана. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database error" xml:space="preserve"> <source>Database error</source> + <target>Грешка в базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> <source>Database is encrypted using a random passphrase, you can change it.</source> + <target>Базата данни е криптирана с автоматично генерирана парола, можете да я промените.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target>Базата данни е криптирана с автоматично генерирана парола. Моля, променете я преди експортиране.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase" xml:space="preserve"> <source>Database passphrase</source> + <target>Парола за базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase & export" xml:space="preserve"> <source>Database passphrase & export</source> + <target>Парола за базата данни и експортиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> <source>Database passphrase is different from saved in the keychain.</source> + <target>Паролата на базата данни е различна от записаната в Keychain.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> <source>Database passphrase is required to open chat.</source> + <target>Изисква се паролата за базата данни, за да се отвори чата.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database upgrade" xml:space="preserve"> <source>Database upgrade</source> + <target>Актуализация на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> <source>Database will be encrypted and the passphrase stored in the keychain. </source> + <target>Базата данни ще бъде криптирана и паролата ще бъде съхранена в Keychain. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be encrypted. " xml:space="preserve"> <source>Database will be encrypted. </source> + <target>Базата данни ще бъде криптирана. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> <source>Database will be migrated when the app restarts</source> + <target>Базата данни ще бъде мигрирана, когато приложението се рестартира</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> + <target>Децентрализиран</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Decryption error" xml:space="preserve"> <source>Decryption error</source> + <target>Грешка при декриптиране</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> + <target>Изтрий</target> <note>chat item action</note> </trans-unit> <trans-unit id="Delete Contact" xml:space="preserve"> <source>Delete Contact</source> + <target>Изтрий контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> + <target>Изтрий адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address?" xml:space="preserve"> <source>Delete address?</source> + <target>Изтрий адрес?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete after" xml:space="preserve"> <source>Delete after</source> + <target>Изтрий след</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete all files" xml:space="preserve"> <source>Delete all files</source> + <target>Изтрий всички файлове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete archive" xml:space="preserve"> <source>Delete archive</source> + <target>Изтрий архив</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat archive?" xml:space="preserve"> <source>Delete chat archive?</source> + <target>Изтриване на архива на чата?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> <source>Delete chat profile</source> + <target>Изтрий чат профила</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> + <target>Изтриване на чат профила?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> + <target>Изтрий връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete contact" xml:space="preserve"> <source>Delete contact</source> + <target>Изтрий контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete contact?" xml:space="preserve"> <source>Delete contact?</source> + <target>Изтрий контакт?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> <source>Delete database</source> + <target>Изтрий базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> + <target>Изтрий файл</target> <note>server test step</note> </trans-unit> <trans-unit id="Delete files and media?" xml:space="preserve"> <source>Delete files and media?</source> + <target>Изтрий файлове и медия?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> <source>Delete files for all chat profiles</source> + <target>Изтрий файловете за всички чат профили</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete for everyone" xml:space="preserve"> <source>Delete for everyone</source> + <target>Изтрий за всички</target> <note>chat feature</note> </trans-unit> <trans-unit id="Delete for me" xml:space="preserve"> <source>Delete for me</source> + <target>Изтрий за мен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete group" xml:space="preserve"> <source>Delete group</source> + <target>Изтрий група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete group?" xml:space="preserve"> <source>Delete group?</source> + <target>Изтрий група?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete invitation" xml:space="preserve"> <source>Delete invitation</source> + <target>Изтрий поканата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete link" xml:space="preserve"> <source>Delete link</source> + <target>Изтрий линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete link?" xml:space="preserve"> <source>Delete link?</source> + <target>Изтрий линк?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> + <target>Изтрий съобщението на члена?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete message?" xml:space="preserve"> <source>Delete message?</source> + <target>Изтрий съобщението?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> + <target>Изтрий съобщенията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> + <target>Изтрий съобщенията след</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database" xml:space="preserve"> <source>Delete old database</source> + <target>Изтрий старата база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database?" xml:space="preserve"> <source>Delete old database?</source> + <target>Изтрий старата база данни?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection" xml:space="preserve"> <source>Delete pending connection</source> + <target>Изтрий предстоящата връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> <source>Delete pending connection?</source> + <target>Изтрий предстоящата връзка?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete profile" xml:space="preserve"> <source>Delete profile</source> + <target>Изтрий профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete queue" xml:space="preserve"> <source>Delete queue</source> + <target>Изтрий опашка</target> <note>server test step</note> </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> + <target>Изтрий потребителския профил?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> + <target>Изтрито на</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Deleted at: %@" xml:space="preserve"> <source>Deleted at: %@</source> + <target>Изтрито на: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Доставка</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> - <source>Delivery receipts will be enabled for all contacts in all visible chat profiles.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delivery receipts will be enabled for all contacts." xml:space="preserve"> - <source>Delivery receipts will be enabled for all contacts.</source> + <target>Потвърждениeто за доставка е деактивирано!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> <source>Delivery receipts!</source> + <target>Потвърждениe за доставка!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Description" xml:space="preserve"> <source>Description</source> + <target>Описание</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> + <target>Разработване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> + <target>Инструменти за разработчици</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device" xml:space="preserve"> <source>Device</source> + <target>Устройство</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> + <target>Идентификацията на устройството е деактивирано. Изключване на SimpleX заключване.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> + <target>Идентификацията на устройството не е активирана. Можете да включите SimpleX заключване през Настройки, след като активирате идентификацията на устройството.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> <source>Different names, avatars and transport isolation.</source> + <target>Различни имена, аватари и транспортна изолация.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Direct messages" xml:space="preserve"> <source>Direct messages</source> + <target>Лични съобщения</target> <note>chat feature</note> </trans-unit> <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> <source>Direct messages between members are prohibited in this group.</source> + <target>Личните съобщения между членовете са забранени в тази група.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve"> + <source>Disable (keep overrides)</source> + <target>Деактивиране (запазване на промените)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> <source>Disable SimpleX Lock</source> + <target>Деактивирай SimpleX заключване</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve"> + <source>Disable for all</source> + <target>Деактивиране за всички</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> <source>Disappearing message</source> + <target>Изчезващо съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappearing messages" xml:space="preserve"> <source>Disappearing messages</source> + <target>Изчезващи съобщения</target> <note>chat feature</note> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> <source>Disappearing messages are prohibited in this chat.</source> + <target>Изчезващите съобщения са забранени в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> <source>Disappearing messages are prohibited in this group.</source> + <target>Изчезващите съобщения са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at" xml:space="preserve"> <source>Disappears at</source> + <target>Изчезва в</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at: %@" xml:space="preserve"> <source>Disappears at: %@</source> + <target>Изчезва в: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> <source>Disconnect</source> + <target>Прекъсни връзката</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Открийте и се присъединете към групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> + <target>Показвано Име</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Display name:" xml:space="preserve"> <source>Display name:</source> + <target>Показвано име:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> + <target>НЕ използвайте SimpleX за спешни повиквания.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> + <target>Отложи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> + <target>Не създавай адрес</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve"> + <source>Don't enable</source> + <target>Не активирай</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> + <target>Не показвай отново</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> + <target>Понижи версията и отвори чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> + <target>Свали файл</target> <note>server test step</note> </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> + <target>Дублирано показвано име!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Duration" xml:space="preserve"> <source>Duration</source> + <target>Продължителност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> + <target>Редактирай</target> <note>chat item action</note> </trans-unit> <trans-unit id="Edit group profile" xml:space="preserve"> <source>Edit group profile</source> + <target>Редактирай групов профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable" xml:space="preserve"> <source>Enable</source> + <target>Активирай</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve"> + <source>Enable (keep overrides)</source> + <target>Активиране (запазване на промените)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> + <target>Активирай SimpleX заключване</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> <source>Enable TCP keep-alive</source> + <target>Активирай TCP keep-alive</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> + <target>Активиране на автоматично изтриване на съобщения?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve"> + <source>Enable for all</source> + <target>Активиране за всички</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable later via Settings" xml:space="preserve"> - <source>Enable later via Settings</source> + <target>Активирай незабавни известия?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable lock" xml:space="preserve"> <source>Enable lock</source> + <target>Активирай заключване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable notifications" xml:space="preserve"> <source>Enable notifications</source> + <target>Активирай известията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable periodic notifications?" xml:space="preserve"> <source>Enable periodic notifications?</source> + <target>Активирай периодични известия?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable self-destruct" xml:space="preserve"> <source>Enable self-destruct</source> + <target>Активирай самоунищожение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> <source>Enable self-destruct passcode</source> + <target>Активирай kод за достъп за самоунищожение</target> <note>set passcode view</note> </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> + <target>Криптирай</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypt database?" xml:space="preserve"> <source>Encrypt database?</source> + <target>Криптиране на база данни?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Криптирай локални файлове</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Криптиране на съхранените файлове и медия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> + <target>Криптирана база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted message or another event" xml:space="preserve"> <source>Encrypted message or another event</source> + <target>Криптирано съобщение или друго събитие</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> + <target>Криптирано съобщение: грешка в базата данни</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> <source>Encrypted message: database migration error</source> + <target>Криптирано съобщение: грешка при мигрирането на база данни</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> <source>Encrypted message: keychain error</source> + <target>Криптирано съобщение: грешка в keychain</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> <source>Encrypted message: no passphrase</source> + <target>Криптирано съобщение: няма парола</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> <source>Encrypted message: unexpected error</source> + <target>Криптирано съобщение: неочаквана грешка</target> <note>notification</note> </trans-unit> <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> + <target>Въведете kодa за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter correct passphrase." xml:space="preserve"> <source>Enter correct passphrase.</source> + <target>Въведи правилна парола.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> + <target>Въведи парола…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter password above to show!" xml:space="preserve"> <source>Enter password above to show!</source> + <target>Въведете парола по-горе, за да се покаже!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter server manually" xml:space="preserve"> <source>Enter server manually</source> + <target>Въведи сървъра ръчно</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter welcome message…" xml:space="preserve"> <source>Enter welcome message…</source> + <target>Въведи съобщение при посрещане…</target> <note>placeholder</note> </trans-unit> <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> <source>Enter welcome message… (optional)</source> + <target>Въведи съобщение при посрещане…(незадължително)</target> <note>placeholder</note> </trans-unit> <trans-unit id="Error" xml:space="preserve"> <source>Error</source> + <target>Грешка при свързване със сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error aborting address change" xml:space="preserve"> <source>Error aborting address change</source> + <target>Грешка при отказване на промяна на адреса</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> + <target>Грешка при приемане на заявка за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error accessing database file" xml:space="preserve"> <source>Error accessing database file</source> + <target>Грешка при достъпа до файла с базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> + <target>Грешка при добавяне на член(ове)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> + <target>Грешка при промяна на адреса</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> + <target>Грешка при промяна на ролята</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing setting" xml:space="preserve"> <source>Error changing setting</source> + <target>Грешка при промяна на настройката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> + <target>Грешка при създаване на адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating group" xml:space="preserve"> <source>Error creating group</source> + <target>Грешка при създаване на група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating group link" xml:space="preserve"> <source>Error creating group link</source> + <target>Грешка при създаване на групов линк</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> + <target>Грешка при създаване на профил!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Грешка при декриптирането на файла</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> + <target>Грешка при изтриване на чат базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat!" xml:space="preserve"> <source>Error deleting chat!</source> + <target>Грешка при изтриването на чата!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting connection" xml:space="preserve"> <source>Error deleting connection</source> + <target>Грешка при изтриване на връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting contact" xml:space="preserve"> <source>Error deleting contact</source> + <target>Грешка при изтриване на контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> + <target>Грешка при изтриване на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting old database" xml:space="preserve"> <source>Error deleting old database</source> + <target>Грешка при изтриване на старата база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting token" xml:space="preserve"> <source>Error deleting token</source> + <target>Грешка при изтриването на токена</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting user profile" xml:space="preserve"> <source>Error deleting user profile</source> + <target>Грешка при изтриване на потребителския профил</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> + <source>Error enabling delivery receipts!</source> + <target>Грешка при активирането на потвърждениeто за доставка!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error enabling notifications" xml:space="preserve"> <source>Error enabling notifications</source> + <target>Грешка при активирането на известията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error encrypting database" xml:space="preserve"> <source>Error encrypting database</source> + <target>Грешка при криптиране на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error exporting chat database" xml:space="preserve"> <source>Error exporting chat database</source> + <target>Грешка при експортиране на чат базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> + <target>Грешка при импортиране на чат базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error joining group" xml:space="preserve"> <source>Error joining group</source> + <target>Грешка при присъединяване към група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error loading %@ servers" xml:space="preserve"> <source>Error loading %@ servers</source> + <target>Грешка при зареждане на %@ сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> + <target>Грешка при получаване на файл</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> + <target>Грешка при отстраняване на член</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving %@ servers" xml:space="preserve"> <source>Error saving %@ servers</source> + <target>Грешка при запазване на %@ сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> <source>Error saving ICE servers</source> + <target>Грешка при запазване на ICE сървърите</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> + <target>Грешка при запазване на профила на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving passcode" xml:space="preserve"> <source>Error saving passcode</source> + <target>Грешка при запазване на кода за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> <source>Error saving passphrase to keychain</source> + <target>Грешка при запазване на парола в Кeychain</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> + <target>Грешка при запазване на потребителска парола</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> + <target>Грешка при изпращане на имейл</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> + <target>Грешка при изпращане на съобщение</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> + <source>Error setting delivery receipts!</source> + <target>Грешка при настройването на потвърждениeто за доставка!!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> <source>Error starting chat</source> + <target>Грешка при стартиране на чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error stopping chat" xml:space="preserve"> <source>Error stopping chat</source> + <target>Грешка при спиране на чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> + <target>Грешка при смяна на профил!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> + <target>Грешка при синхронизиране на връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> + <target>Грешка при актуализиране на груповия линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating message" xml:space="preserve"> <source>Error updating message</source> + <target>Грешка при актуализиране на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> + <target>Грешка при актуализиране на настройките</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating user privacy" xml:space="preserve"> <source>Error updating user privacy</source> + <target>Грешка при актуализиране на поверителността на потребителя</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> + <target>Грешка: </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> + <target>Грешка: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> + <target>Грешка: URL адресът е невалиден</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: no database file" xml:space="preserve"> <source>Error: no database file</source> + <target>Грешка: няма файл с база данни</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> + <source>Even when disabled in the conversation.</source> + <target>Дори когато е деактивиран в разговора.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exit without saving" xml:space="preserve"> <source>Exit without saving</source> + <target>Изход без запазване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> + <target>Експортирай база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Export error:" xml:space="preserve"> <source>Export error:</source> + <target>Грешка при експортиране:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve"> - <source>Exporting database archive...</source> + <target>Експортиран архив на базата данни.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exporting database archive…" xml:space="preserve"> <source>Exporting database archive…</source> + <target>Експортиране на архив на базата данни…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Failed to remove passphrase" xml:space="preserve"> <source>Failed to remove passphrase</source> + <target>Премахването на паролата е неуспешно</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> <source>Fast and no wait until the sender is online!</source> + <target>Бързо и без чакане, докато подателят е онлайн!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> + <target>Любим</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> + <target>Файлът ще бъде изтрит от сървърите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> <source>File will be received when your contact completes uploading it.</source> + <target>Файлът ще бъде получен, когато вашият контакт завърши качването му.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> + <target>Файлът ще бъде получен, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File: %@" xml:space="preserve"> <source>File: %@</source> + <target>Файл: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> + <target>Файлове и медия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media" xml:space="preserve"> <source>Files and media</source> + <target>Файлове и медия</target> <note>chat feature</note> </trans-unit> <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> <source>Files and media are prohibited in this group.</source> + <target>Файловете и медията са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> + <target>Файловете и медията са забранени!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> + <source>Filter unread and favorite chats.</source> + <target>Филтрирайте непрочетените и любимите чатове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> + <target>Най-накрая ги имаме! 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Find chats faster" xml:space="preserve"> + <source>Find chats faster</source> + <target>Намирайте чатове по-бързо</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> <source>Fix</source> + <target>Поправи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection" xml:space="preserve"> <source>Fix connection</source> + <target>Поправи връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection?" xml:space="preserve"> <source>Fix connection?</source> + <target>Поправи връзката?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> + <source>Fix encryption after restoring backups.</source> + <target>Оправяне на криптирането след възстановяване от резервни копия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by contact" xml:space="preserve"> <source>Fix not supported by contact</source> + <target>Поправката не се поддържа от контакта</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by group member" xml:space="preserve"> <source>Fix not supported by group member</source> + <target>Поправката не се поддържа от члена на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> + <target>За конзолата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> <source>French interface</source> + <target>Френски интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full link" xml:space="preserve"> <source>Full link</source> + <target>Цял линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full name (optional)" xml:space="preserve"> <source>Full name (optional)</source> + <target>Пълно име (незадължително)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full name:" xml:space="preserve"> <source>Full name:</source> + <target>Пълно име:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <source>Fully re-implemented - work in background!</source> + <target>Напълно преработено - работи във фонов режим!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Further reduced battery usage" xml:space="preserve"> <source>Further reduced battery usage</source> + <target>Допълнително намален разход на батерията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="GIFs and stickers" xml:space="preserve"> <source>GIFs and stickers</source> + <target>GIF файлове и стикери</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> + <target>Група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> <source>Group display name</source> + <target>Показвано име на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group full name (optional)" xml:space="preserve"> <source>Group full name (optional)</source> + <target>Пълно име на групата (незадължително)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group image" xml:space="preserve"> <source>Group image</source> + <target>Групово изображение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation" xml:space="preserve"> <source>Group invitation</source> + <target>Групова покана</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation expired" xml:space="preserve"> <source>Group invitation expired</source> + <target>Груповата покана е изтекла</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> <source>Group invitation is no longer valid, it was removed by sender.</source> + <target>Груповата покана вече е невалидна, премахната е от подателя.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group link" xml:space="preserve"> <source>Group link</source> + <target>Групов линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group links" xml:space="preserve"> <source>Group links</source> + <target>Групови линкове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can add message reactions." xml:space="preserve"> <source>Group members can add message reactions.</source> + <target>Членовете на групата могат да добавят реакции към съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> <source>Group members can irreversibly delete sent messages.</source> + <target>Членовете на групата могат необратимо да изтриват изпратените съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send direct messages." xml:space="preserve"> <source>Group members can send direct messages.</source> + <target>Членовете на групата могат да изпращат лични съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> <source>Group members can send disappearing messages.</source> + <target>Членовете на групата могат да изпращат изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send files and media." xml:space="preserve"> <source>Group members can send files and media.</source> + <target>Членовете на групата могат да изпращат файлове и медия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send voice messages." xml:space="preserve"> <source>Group members can send voice messages.</source> + <target>Членовете на групата могат да изпращат гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> + <target>Групово съобщение:</target> <note>notification</note> </trans-unit> <trans-unit id="Group moderation" xml:space="preserve"> <source>Group moderation</source> + <target>Групово модериране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group preferences" xml:space="preserve"> <source>Group preferences</source> + <target>Групови настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile" xml:space="preserve"> <source>Group profile</source> + <target>Групов профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> <source>Group profile is stored on members' devices, not on the servers.</source> + <target>Груповият профил се съхранява на устройствата на членовете, а не на сървърите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group welcome message" xml:space="preserve"> <source>Group welcome message</source> + <target>Съобщение при посрещане в групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> + <target>Групата ще бъде изтрита за всички членове - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for you - this cannot be undone!</source> + <target>Групата ще бъде изтрита за вас - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> + <target>Помощ</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> + <target>Скрит</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden chat profiles" xml:space="preserve"> <source>Hidden chat profiles</source> + <target>Скрити чат профили</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden profile password" xml:space="preserve"> <source>Hidden profile password</source> + <target>Парола за скрит профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide" xml:space="preserve"> <source>Hide</source> + <target>Скрий</target> <note>chat item action</note> </trans-unit> <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> <source>Hide app screen in the recent apps.</source> + <target>Скриване на екрана на приложението в изгледа на скоро отворнените приложения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide profile" xml:space="preserve"> <source>Hide profile</source> + <target>Скрий профила</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide:" xml:space="preserve"> <source>Hide:</source> + <target>Скрий:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="History" xml:space="preserve"> <source>History</source> - <note>copied message info</note> + <target>История</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> + <target>Как работи SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> + <target>Как работи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> + <target>Информация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use it" xml:space="preserve"> <source>How to use it</source> + <target>Как се използва</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use your servers" xml:space="preserve"> <source>How to use your servers</source> + <target>Как да използвате вашите сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> + <target>ICE сървъри (по един на ред)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> + <target>Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> + <target>Ако не можете да се срещнете на живо, можете да **сканирате QR код във видеообаждането** или вашият контакт може да сподели линк за покана.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> + <target>Ако въведете този kод за достъп, когато отваряте приложението, всички данни от приложението ще бъдат необратимо изтрити!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> <source>If you enter your self-destruct passcode while opening the app:</source> + <target>Ако въведете kодa за достъп за самоунищожение, докато отваряте приложението:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> + <target>Ако трябва да използвате чата сега, докоснете **Отложи** отдолу (ще ви бъде предложено да мигрирате базата данни, когато рестартирате приложението).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Ignore" xml:space="preserve"> <source>Ignore</source> + <target>Игнорирай</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> <source>Image will be received when your contact completes uploading it.</source> + <target>Изображението ще бъде получено, когато вашият контакт завърши качването му.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Image will be received when your contact is online, please wait or check later!</source> + <target>Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Immediately" xml:space="preserve"> <source>Immediately</source> + <target>Веднага</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Immune to spam and abuse" xml:space="preserve"> <source>Immune to spam and abuse</source> + <target>Защитен от спам и злоупотреби</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> <source>Import</source> + <target>Импортиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import chat database?" xml:space="preserve"> <source>Import chat database?</source> + <target>Импортиране на чат база данни?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import database" xml:space="preserve"> <source>Import database</source> + <target>Импортиране на база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> + <target>Подобрена поверителност и сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved server configuration" xml:space="preserve"> <source>Improved server configuration</source> + <target>Подобрена конфигурация на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> - <note>copied message info</note> + <target>В отговор на</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> + <target>Инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> <source>Incognito mode</source> + <target>Режим инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Режимът инкогнито защитава вашата поверителност, като използва нов автоматично генериран профил за всеки контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> <source>Incoming audio call</source> + <target>Входящо аудио повикване</target> <note>notification</note> </trans-unit> <trans-unit id="Incoming call" xml:space="preserve"> <source>Incoming call</source> + <target>Входящо повикване</target> <note>notification</note> </trans-unit> <trans-unit id="Incoming video call" xml:space="preserve"> <source>Incoming video call</source> + <target>Входящо видео повикване</target> <note>notification</note> </trans-unit> <trans-unit id="Incompatible database version" xml:space="preserve"> <source>Incompatible database version</source> + <target>Несъвместима версия на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incorrect passcode" xml:space="preserve"> <source>Incorrect passcode</source> + <target>Неправилен kод за достъп</target> <note>PIN entry</note> </trans-unit> <trans-unit id="Incorrect security code!" xml:space="preserve"> <source>Incorrect security code!</source> + <target>Неправилен код за сигурност!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Info" xml:space="preserve"> <source>Info</source> + <target>Информация</target> <note>chat item action</note> </trans-unit> <trans-unit id="Initial role" xml:space="preserve"> <source>Initial role</source> + <target>Първоначална роля</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> + <target>Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> + <target>Незабавните push известия ще бъдат скрити! +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Instantly" xml:space="preserve"> <source>Instantly</source> + <target>Мигновено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> + <target>Интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> + <target>Невалиден линк за връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> + <target>Невалиден адрес на сървъра!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Невалиден статус</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> + <target>Поканата е изтекла!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite friends" xml:space="preserve"> <source>Invite friends</source> + <target>Покани приятели</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite members" xml:space="preserve"> <source>Invite members</source> + <target>Покани членове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> + <target>Покани в групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion" xml:space="preserve"> <source>Irreversible message deletion</source> + <target>Необратимо изтриване на съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this chat.</source> + <target>Необратимото изтриване на съобщения е забранено в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this group.</source> + <target>Необратимото изтриване на съобщения е забранено в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> + <target>Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил .</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> <source>It can happen when you or your connection used the old database backup.</source> + <target>Това може да се случи, когато вие или вашата връзка използвате старо резервно копие на базата данни.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> @@ -2110,2737 +2778,3542 @@ 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised.</source> + <target>Това може да се случи, когато: +1. Времето за пазене на съобщенията е изтекло - в изпращащия клиент е 2 дена а на сървъра е 30. +2. Декриптирането на съобщението е неуспешно, защото вие или вашият контакт сте използвали старо копие на базата данни. +3. Връзката е била компрометирана.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> + <target>Изглежда, че вече сте свързани чрез този линк. Ако не е така, има грешка (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Italian interface" xml:space="preserve"> <source>Italian interface</source> + <target>Италиански интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Japanese interface" xml:space="preserve"> <source>Japanese interface</source> + <target>Японски интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> + <target>Присъединяване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> + <target>Влез в групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> <source>Join incognito</source> + <target>Влез инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> <source>Joining group</source> + <target>Присъединяване към групата</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep your connections" xml:space="preserve"> + <source>Keep your connections</source> + <target>Запазете връзките си</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="KeyChain error" xml:space="preserve"> <source>KeyChain error</source> + <target>KeyChain грешка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Keychain error" xml:space="preserve"> <source>Keychain error</source> + <target>Keychain грешка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="LIVE" xml:space="preserve"> <source>LIVE</source> + <target>НА ЖИВО</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Large file!" xml:space="preserve"> <source>Large file!</source> + <target>Голям файл!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Learn more" xml:space="preserve"> <source>Learn more</source> + <target>Научете повече</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> + <target>Напусни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> <source>Leave group</source> + <target>Напусни групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group?" xml:space="preserve"> <source>Leave group?</source> + <target>Напусни групата?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> <source>Let's talk in SimpleX Chat</source> + <target>Нека да поговорим в SimpleX Chat</target> <note>email subject</note> </trans-unit> <trans-unit id="Light" xml:space="preserve"> <source>Light</source> + <target>Светла</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Limitations" xml:space="preserve"> <source>Limitations</source> + <target>Ограничения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> + <target>Съобщение на живо!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live messages" xml:space="preserve"> <source>Live messages</source> + <target>Съобщения на живо</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local name" xml:space="preserve"> <source>Local name</source> + <target>Локално име</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local profile data only" xml:space="preserve"> <source>Local profile data only</source> + <target>Само данни за локален профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Lock after" xml:space="preserve"> <source>Lock after</source> + <target>Заключване след</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Lock mode" xml:space="preserve"> <source>Lock mode</source> + <target>Режим на заключване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make a private connection" xml:space="preserve"> <source>Make a private connection</source> + <target>Добави поверителна връзка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make one message disappear" xml:space="preserve"> + <source>Make one message disappear</source> + <target>Накарайте едно съобщение да изчезне</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make profile private!" xml:space="preserve"> <source>Make profile private!</source> + <target>Направи профила поверителен!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> + <target>Уверете се, че %@ сървърните адреси са в правилен формат, разделени на редове и не се дублират (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> + <target>Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> + <target>Много хора попитаха: *ако SimpleX няма потребителски идентификатори, как може да доставя съобщения?*</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> + <target>Маркирай като изтрито за всички</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark read" xml:space="preserve"> <source>Mark read</source> + <target>Маркирай като прочетено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark verified" xml:space="preserve"> <source>Mark verified</source> + <target>Маркирай като проверено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Markdown in messages" xml:space="preserve"> <source>Markdown in messages</source> + <target>Форматиране на съобщения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> <source>Max 30 seconds, received instantly.</source> + <target>Макс. 30 секунди, получено незабавно.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> + <target>Член</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> + <target>Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> + <target>Ролята на члена ще бъде променена на "%@". Членът ще получи нова покана.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> + <target>Членът ще бъде премахнат от групата - това не може да бъде отменено!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> + <target>Грешка при доставката на съобщението</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve"> + <source>Message delivery receipts!</source> + <target>Потвърждениe за доставка на съобщения!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> + <target>Чернова на съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> + <target>Реакции на съобщения</target> <note>chat feature</note> </trans-unit> <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> <source>Message reactions are prohibited in this chat.</source> + <target>Реакциите на съобщения са забранени в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> <source>Message reactions are prohibited in this group.</source> + <target>Реакциите на съобщения са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> + <target>Текст на съобщението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> + <target>Съобщения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Messages & files" xml:space="preserve"> <source>Messages & files</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve"> - <source>Migrating database archive...</source> + <target>Съобщения и файлове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive…" xml:space="preserve"> <source>Migrating database archive…</source> + <target>Архивът на базата данни се мигрира…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> + <target>Грешка при мигриране:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> + <target>Мигрирането е неуспешно. Докоснете **Пропускане** по-долу, за да продължите да използвате текущата база данни. Моля, докладвайте проблема на разработчиците на приложението чрез чат или имейл [chat@simplex.chat](mailto:chat@simplex.chat).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration is completed" xml:space="preserve"> <source>Migration is completed</source> + <target>Миграцията е завършена</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrations: %@" xml:space="preserve"> <source>Migrations: %@</source> + <target>Миграции: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> <source>Moderate</source> + <target>Модерирай</target> <note>chat item action</note> </trans-unit> <trans-unit id="Moderated at" xml:space="preserve"> <source>Moderated at</source> + <target>Модерирано в</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderated at: %@" xml:space="preserve"> <source>Moderated at: %@</source> + <target>Модерирано в: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> + <target>Очаквайте скоро още подобрения!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Най-вероятно тази връзка е изтрита.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> + <target>Най-вероятно този контакт е изтрил връзката с вас.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> + <target>Множество профили за чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> + <target>Без звук</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> + <target>Без звук при неактивност!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> + <target>Име</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> + <target>Мрежа и сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> + <target>Мрежови настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network status" xml:space="preserve"> <source>Network status</source> + <target>Състояние на мрежата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> + <target>Нов kод за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> + <target>Нова заявка за контакт</target> <note>notification</note> </trans-unit> <trans-unit id="New contact:" xml:space="preserve"> <source>New contact:</source> + <target>Нов контакт:</target> <note>notification</note> </trans-unit> <trans-unit id="New database archive" xml:space="preserve"> <source>New database archive</source> + <target>Нов архив на база данни</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Ново настолно приложение!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> + <target>Ново показвано име</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> + <target>Ново в %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> + <target>Нова членска роля</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New message" xml:space="preserve"> <source>New message</source> + <target>Ново съобщение</target> <note>notification</note> </trans-unit> <trans-unit id="New passphrase…" xml:space="preserve"> <source>New passphrase…</source> + <target>Нова парола…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> + <target>Не</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No app password" xml:space="preserve"> <source>No app password</source> + <target>Приложението няма kод за достъп</target> <note>Authentication unavailable</note> </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> + <target>Няма избрани контакти</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No contacts to add" xml:space="preserve"> <source>No contacts to add</source> + <target>Няма контакти за добавяне</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Няма информация за доставката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> + <target>Няма токен за устройство!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> + <target>Няма филтрирани чатове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No group!" xml:space="preserve"> <source>Group not found!</source> + <target>Групата не е намерена!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No history" xml:space="preserve"> <source>No history</source> + <target>Няма история</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> + <target>Няма разрешение за запис на гласово съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> + <target>Няма получени или изпратени файлове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> + <target>Известия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Notifications are disabled!" xml:space="preserve"> <source>Notifications are disabled!</source> + <target>Известията са деактивирани!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> + <target>Сега администраторите могат: +- да изтриват съобщения на членове. +- да деактивират членове (роля "наблюдател")</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Off" xml:space="preserve"> <source>Off</source> + <target>Изключено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Off (Local)" xml:space="preserve"> <source>Off (Local)</source> + <target>Изключено (Локално)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> + <target>Ок</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> + <target>Стара база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Old database archive" xml:space="preserve"> <source>Old database archive</source> + <target>Стар архив на база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> + <target>Линк за еднократна покана</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> <source>Onion hosts will be required for connection. Requires enabling VPN.</source> + <target>За свързване ще са необходими Onion хостове. Изисква се активиране на VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> <source>Onion hosts will be used when available. Requires enabling VPN.</source> + <target>Ще се използват Onion хостове, когато са налични. Изисква се активиране на VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> <source>Onion hosts will not be used.</source> + <target>Няма се използват Onion хостове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <target>Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> + <target>Само собствениците на групата могат да променят груповите настройки.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> <source>Only group owners can enable files and media.</source> + <target>Само собствениците на групата могат да активират файлове и медията.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> <source>Only group owners can enable voice messages.</source> + <target>Само собствениците на групата могат да активират гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> + <target>Само вие можете да добавяте реакции на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> + <target>Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> <source>Only you can make calls.</source> + <target>Само вие можете да извършвате разговори.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> <source>Only you can send disappearing messages.</source> + <target>Само вие можете да изпращате изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can send voice messages." xml:space="preserve"> <source>Only you can send voice messages.</source> + <target>Само вие можете да изпращате гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> <source>Only your contact can add message reactions.</source> + <target>Само вашият контакт може да добавя реакции на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> + <target>Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> <source>Only your contact can make calls.</source> + <target>Само вашият контакт може да извършва разговори.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> <source>Only your contact can send disappearing messages.</source> + <target>Само вашият контакт може да изпраща изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> <source>Only your contact can send voice messages.</source> + <target>Само вашият контакт може да изпраща гласови съобщения.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> + <target>Отвори настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> + <target>Отвори чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat console" xml:space="preserve"> <source>Open chat console</source> + <target>Отвори конзолата</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Open user profiles" xml:space="preserve"> <source>Open user profiles</source> + <target>Отвори потребителските профили</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> <source>Open-source protocol and code – anybody can run the servers.</source> + <target>Протокол и код с отворен код – всеки може да оперира собствени сървъри.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening database…" xml:space="preserve"> <source>Opening database…</source> + <target>Отваряне на база данни…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> + <target>Отварянето на линка в браузъра може да намали поверителността и сигурността на връзката. Несигурните SimpleX линкове ще бъдат червени.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> + <target>PING бройка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="PING interval" xml:space="preserve"> <source>PING interval</source> + <target>PING интервал</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode" xml:space="preserve"> <source>Passcode</source> + <target>Код за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode changed!" xml:space="preserve"> <source>Passcode changed!</source> + <target>Кодът за достъп е променен!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode entry" xml:space="preserve"> <source>Passcode entry</source> + <target>Въвеждане на код за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode not changed!" xml:space="preserve"> <source>Passcode not changed!</source> + <target>Кодът за достъп не е променен!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Passcode set!" xml:space="preserve"> <source>Passcode set!</source> + <target>Кодът за достъп е зададен!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> + <target>Парола за показване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste" xml:space="preserve"> <source>Paste</source> + <target>Постави</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> <source>Paste image</source> + <target>Постави изображение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste received link" xml:space="preserve"> <source>Paste received link</source> + <target>Постави получения линк</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <note>No comment provided by engineer.</note> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> + <target>Поставете линка, който сте получили, за да се свържете с вашия контакт.</target> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> + <target>Хората могат да се свържат с вас само чрез ликовете, които споделяте.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> <source>Periodically</source> + <target>Периодично</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Permanent decryption error" xml:space="preserve"> <source>Permanent decryption error</source> + <target>Постоянна грешка при декриптиране</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> + <target>Моля, попитайте вашия контакт, за да активирате изпращане на гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> + <target>Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> <source>Please check your network connection with %@ and try again.</source> + <target>Моля, проверете мрежовата си връзка с %@ и опитайте отново.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> <source>Please check yours and your contact preferences.</source> + <target>Моля, проверете вашите настройки и тези вашия за контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> + <target>Моля, свържете се с груповия администартор.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> <source>Please enter correct current passphrase.</source> + <target>Моля, въведете правилната текуща парола.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> + <target>Моля, въведете предишната парола след възстановяване на резервното копие на базата данни. Това действие не може да бъде отменено.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> + <target>Моля, запомнете го или го съхранявайте на сигурно място - няма начин да възстановите изгубен код за достъп!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please report it to the developers." xml:space="preserve"> <source>Please report it to the developers.</source> + <target>Моля, докладвайте го на разработчиците.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> <source>Please restart the app and migrate the database to enable push notifications.</source> + <target>Моля, рестартирайте приложението и мигрирайте базата данни, за да активирате push известия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> + <target>Моля, съхранявайте паролата на сигурно място, НЯМА да имате достъп до чата, ако я загубите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> + <target>Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> + <target>Полски интерфейс</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> + <target>Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен</target> <note>server test error</note> </trans-unit> <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> <source>Preserve the last message draft, with attachments.</source> + <target>Запазете последната чернова на съобщението с прикачени файлове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preset server" xml:space="preserve"> <source>Preset server</source> + <target>Предварително зададен сървър</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> + <target>Предварително зададен адрес на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> + <target>Визуализация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> + <target>Поверителност и сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> + <target>Поверителността преосмислена</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> + <target>Поверителни имена на файлове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> + <target>Профилни и сървърни връзки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile image" xml:space="preserve"> <source>Profile image</source> + <target>Профилно изображение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile password" xml:space="preserve"> <source>Profile password</source> + <target>Профилна парола</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> + <target>Актуализацията на профила ще бъде изпратена до вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> + <target>Забрани аудио/видео разговорите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> <source>Prohibit irreversible message deletion.</source> + <target>Забрани необратимото изтриване на съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit message reactions." xml:space="preserve"> <source>Prohibit message reactions.</source> + <target>Забрани реакциите на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit messages reactions." xml:space="preserve"> <source>Prohibit messages reactions.</source> + <target>Забрани реакциите на съобщенията.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> + <target>Забрани изпращането на лични съобщения до членовете.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> <source>Prohibit sending disappearing messages.</source> + <target>Забрани изпращането на изчезващи съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending files and media." xml:space="preserve"> <source>Prohibit sending files and media.</source> + <target>Забрани изпращането на файлове и медия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> <source>Prohibit sending voice messages.</source> + <target>Забрани изпращането на гласови съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> + <target>Защити екрана на приложението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> + <target>Защитете чат профилите с парола!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout" xml:space="preserve"> <source>Protocol timeout</source> + <target>Време за изчакване на протокола</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout per KB" xml:space="preserve"> <source>Protocol timeout per KB</source> + <target>Време за изчакване на протокола за KB</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> + <target>Push известия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> + <target>Оценете приложението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> + <target>Реагирай…</target> <note>chat item menu</note> </trans-unit> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> + <target>Прочетено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> + <target>Прочетете още</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> + <target>Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> + <target>Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> <source>Read more in our GitHub repository.</source> + <target>Прочетете повече в нашето хранилище в GitHub.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> + <target>Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Потвърждениeто за доставка е деактивирано</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> + <target>Получено в</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Received at: %@" xml:space="preserve"> <source>Received at: %@</source> + <target>Получено в: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Received file event" xml:space="preserve"> <source>Received file event</source> + <target>Събитие за получен файл</target> <note>notification</note> </trans-unit> <trans-unit id="Received message" xml:space="preserve"> <source>Received message</source> + <target>Получено съобщение</target> <note>message info title</note> </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving file will be stopped." xml:space="preserve"> <source>Receiving file will be stopped.</source> + <target>Получаващият се файл ще бъде спрян.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving via" xml:space="preserve"> <source>Receiving via</source> + <target>Получаване чрез</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> + <target>Получателите виждат актуализации, докато ги въвеждате.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>Повторно се свържете с всички свързани сървъри, за да принудите доставката на съобщенията. Използва се допълнителен трафик.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> + <target>Повторно свърване със сървърите?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> <source>Record updated at</source> + <target>Записът е актуализиран на</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at: %@" xml:space="preserve"> <source>Record updated at: %@</source> + <target>Записът е актуализиран на: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Reduced battery usage" xml:space="preserve"> <source>Reduced battery usage</source> + <target>Намалена консумация на батерията</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> + <target>Отхвърляне</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> + <target>Отхвърляне (подателят НЕ бива уведомен)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject contact request" xml:space="preserve"> <source>Reject contact request</source> + <target>Отхвърли заявката за контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> <source>Relay server is only used if necessary. Another party can observe your IP address.</source> + <target>Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> <source>Relay server protects your IP address, but it can observe the duration of the call.</source> + <target>Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove" xml:space="preserve"> <source>Remove</source> + <target>Премахване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> + <target>Острани член</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member?" xml:space="preserve"> <source>Remove member?</source> + <target>Острани член?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> <source>Remove passphrase from keychain?</source> + <target>Премахване на паролата от keychain?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate" xml:space="preserve"> <source>Renegotiate</source> + <target>Предоговоряне</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption" xml:space="preserve"> <source>Renegotiate encryption</source> + <target>Предоговори криптирането</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption?" xml:space="preserve"> <source>Renegotiate encryption?</source> + <target>Предоговори криптирането?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> + <target>Отговори</target> <note>chat item action</note> </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> + <target>Задължително</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset" xml:space="preserve"> <source>Reset</source> + <target>Нулиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> + <target>Нулирай цветовете</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> + <target>Възстановяване на настройките по подразбиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> + <target>Рестартирайте приложението, за да създадете нов чат профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> <source>Restart the app to use imported chat database</source> + <target>Рестартирайте приложението, за да използвате импортирана чат база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore" xml:space="preserve"> <source>Restore</source> + <target>Възстанови</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database backup" xml:space="preserve"> <source>Restore database backup</source> + <target>Възстанови резервно копие на база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database backup?" xml:space="preserve"> <source>Restore database backup?</source> + <target>Възстанови резервно копие на база данни?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database error" xml:space="preserve"> <source>Restore database error</source> + <target>Грешка при възстановяване на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> + <target>Покажи</target> <note>chat item action</note> </trans-unit> <trans-unit id="Revert" xml:space="preserve"> <source>Revert</source> + <target>Отмени промените</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke" xml:space="preserve"> <source>Revoke</source> + <target>Отзови</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Revoke file" xml:space="preserve"> <source>Revoke file</source> + <target>Отзови файл</target> <note>cancel file action</note> </trans-unit> <trans-unit id="Revoke file?" xml:space="preserve"> <source>Revoke file?</source> + <target>Отзови файл?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Role" xml:space="preserve"> <source>Role</source> + <target>Роля</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Run chat" xml:space="preserve"> <source>Run chat</source> + <target>Стартиране на чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SMP servers" xml:space="preserve"> <source>SMP servers</source> + <target>SMP сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> + <target>Запази</target> <note>chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> + <target>Запази (и уведоми контактите)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> + <target>Запази и уведоми контакта</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> + <target>Запази и уведоми членовете на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> + <target>Запази и актуализирай профила на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save archive" xml:space="preserve"> <source>Save archive</source> + <target>Запази архив</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save auto-accept settings" xml:space="preserve"> <source>Save auto-accept settings</source> + <target>Запази настройките за автоматично приемане</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> + <target>Запази профила на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> + <target>Запази паролата и отвори чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> <source>Save passphrase in Keychain</source> + <target>Запази паролата в Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> + <target>Запази настройките?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> + <target>Запази паролата на профила</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers" xml:space="preserve"> <source>Save servers</source> + <target>Запази сървърите</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> + <target>Запази сървърите?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save settings?" xml:space="preserve"> <source>Save settings?</source> + <target>Запази настройките?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> + <target>Запази съобщението при посрещане?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> + <target>Запазените WebRTC ICE сървъри ще бъдат премахнати</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> + <target>Сканирай QR код</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> <source>Scan code</source> + <target>Сканирай код</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> <source>Scan security code from your contact's app.</source> + <target>Сканирайте кода за сигурност от приложението на вашия контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan server QR code" xml:space="preserve"> <source>Scan server QR code</source> + <target>Сканирай QR кода на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Search" xml:space="preserve"> <source>Search</source> + <target>Търсене</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> + <target>Сигурна опашка</target> <note>server test step</note> </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> + <target>Оценка на сигурността</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Security code" xml:space="preserve"> <source>Security code</source> + <target>Код за сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Select" xml:space="preserve"> <source>Select</source> + <target>Избери</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct" xml:space="preserve"> <source>Self-destruct</source> + <target>Самоунищожение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct passcode" xml:space="preserve"> <source>Self-destruct passcode</source> + <target>Код за достъп за самоунищожение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> <source>Self-destruct passcode changed!</source> + <target>Кодът за достъп за самоунищожение е променен!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> <source>Self-destruct passcode enabled!</source> + <target>Кодът за достъп за самоунищожение е активиран!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send" xml:space="preserve"> <source>Send</source> + <target>Изпрати</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> <source>Send a live message - it will update for the recipient(s) as you type it</source> + <target>Изпратете съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send delivery receipts to" xml:space="preserve"> <source>Send delivery receipts to</source> + <target>Изпращайте потвърждениe за доставка на</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send direct message" xml:space="preserve"> <source>Send direct message</source> + <target>Изпрати лично съобщение</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> + <target>Изпрати изчезващо съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> + <target>Изпрати визуализация на линковете</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send live message" xml:space="preserve"> <source>Send live message</source> + <target>Изпрати съобщение на живо</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> + <target>Изпращай известия</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send notifications:" xml:space="preserve"> <source>Send notifications:</source> + <target>Изпратени известия:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> <source>Send questions and ideas</source> + <target>Изпращайте въпроси и идеи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send receipts" xml:space="preserve"> <source>Send receipts</source> + <target>Изпращане на потвърждениe за доставка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> <source>Send them from gallery or custom keyboards.</source> + <target>Изпрати от галерия или персонализирани клавиатури.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> + <target>Подателят отмени прехвърлянето на файла.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> + <target>Подателят може да е изтрил заявката за връзка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending file will be stopped." xml:space="preserve"> <source>Sending file will be stopped.</source> + <target>Изпращането на файла ще бъде спряно.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is disabled for %lld contacts</source> + <target>Изпращането на потвърждениe за доставка е деактивирано за %lld контакта</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Изпращането на потвърждениe за доставка е деактивирано за %lld групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is enabled for %lld contacts</source> + <target>Изпращането на потвърждениe за доставка е активирано за %lld контакта</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Изпращането на потвърждениe за доставка е активирано за %lld групи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> + <target>Изпращане чрез</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sent at" xml:space="preserve"> <source>Sent at</source> + <target>Изпратено на</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sent at: %@" xml:space="preserve"> <source>Sent at: %@</source> + <target>Изпратено на: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> + <target>Събитие за изпратен файл</target> <note>notification</note> </trans-unit> <trans-unit id="Sent message" xml:space="preserve"> <source>Sent message</source> + <target>Изпратено съобщение</target> <note>message info title</note> </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> + <target>Изпратените съобщения ще бъдат изтрити след зададеното време.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> + <target>Сървърът изисква оторизация за създаване на опашки, проверете паролата</target> <note>server test error</note> </trans-unit> <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> <source>Server requires authorization to upload, check password</source> + <target>Сървърът изисква оторизация за качване, проверете паролата</target> <note>server test error</note> </trans-unit> <trans-unit id="Server test failed!" xml:space="preserve"> <source>Server test failed!</source> + <target>Тестът на сървъра е неуспешен!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> + <target>Сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> <source>Set 1 day</source> + <target>Задай 1 ден</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> + <target>Задай име на контакт…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> + <target>Задай групови настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set it instead of system authentication." xml:space="preserve"> <source>Set it instead of system authentication.</source> + <target>Задайте го вместо системната идентификация.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> + <target>Задай kод за достъп</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> + <target>Задай парола за експортиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set the message shown to new members!" xml:space="preserve"> <source>Set the message shown to new members!</source> + <target>Задай съобщението, показано на новите членове!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> <source>Set timeouts for proxy/VPN</source> + <target>Задай време за изчакване за прокси/VPN</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Settings" xml:space="preserve"> <source>Settings</source> + <target>Настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> + <target>Сподели</target> <note>chat item action</note> </trans-unit> <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> + <target>Сподели еднократен линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> + <target>Сподели адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> + <target>Сподели адреса с контактите?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> <source>Share link</source> + <target>Сподели линк</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share one-time invitation link" xml:space="preserve"> <source>Share one-time invitation link</source> + <target>Сподели линк за еднократна покана</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share with contacts" xml:space="preserve"> <source>Share with contacts</source> + <target>Сподели с контактите</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> + <target>Показване на обажданията в хронологията на телефона</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show developer options" xml:space="preserve"> <source>Show developer options</source> + <target>Покажи опциите за разработчици</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Показване на последните съобщения в листа с чатовете</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> + <target>Показване на визуализация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> + <target>Покажи:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> + <target>SimpleX Адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> + <target>Сигурността на SimpleX Chat беше одитирана от Trail of Bits.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock" xml:space="preserve"> <source>SimpleX Lock</source> + <target>SimpleX заключване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock mode" xml:space="preserve"> <source>SimpleX Lock mode</source> + <target>Режим на SimpleX заключване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> <source>SimpleX Lock not enabled!</source> + <target>SimpleX заключване не е активирано!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> <source>SimpleX Lock turned on</source> + <target>SimpleX заключване е включено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX address" xml:space="preserve"> <source>SimpleX address</source> + <target>SimpleX адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> + <target>SimpleX адрес за контакт</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> <source>SimpleX encrypted message or connection event</source> + <target>SimpleX криптирано съобщение или събитие за връзка</target> <note>notification</note> </trans-unit> <trans-unit id="SimpleX group link" xml:space="preserve"> <source>SimpleX group link</source> + <target>SimpleX групов линк</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> + <target>SimpleX линкове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> <source>SimpleX one-time invitation</source> + <target>Еднократна покана за SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Опростен режим инкогнито</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> + <target>Пропускане</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Skipped messages" xml:space="preserve"> <source>Skipped messages</source> + <target>Пропуснати съобщения</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Small groups (max 10)" xml:space="preserve"> - <source>Small groups (max 10)</source> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Малки групи (максимум 20)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> + <target>Някой</target> <note>notification title</note> </trans-unit> <trans-unit id="Start a new chat" xml:space="preserve"> <source>Start a new chat</source> + <target>Започни нов чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> <source>Start chat</source> + <target>Започни чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> + <target>Започни миграция</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> + <target>Спри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop SimpleX" xml:space="preserve"> <source>Stop SimpleX</source> + <target>Спри SimpleX</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> <source>Stop chat to enable database actions</source> + <target>Спрете чата, за да активирате действията с базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> + <target>Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat?" xml:space="preserve"> <source>Stop chat?</source> + <target>Спри чата?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop file" xml:space="preserve"> <source>Stop file</source> + <target>Спри файл</target> <note>cancel file action</note> </trans-unit> <trans-unit id="Stop receiving file?" xml:space="preserve"> <source>Stop receiving file?</source> + <target>Спри получаването на файла?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sending file?" xml:space="preserve"> <source>Stop sending file?</source> + <target>Спри изпращането на файла?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> + <target>Спри споделянето</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> + <target>Спри споделянето на адреса?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> + <target>Изпрати</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> + <target>Подкрепете SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> + <target>Системен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="System authentication" xml:space="preserve"> <source>System authentication</source> + <target>Системна идентификация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> + <target>Времето на изчакване за установяване на TCP връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> + <target>TCP_KEEPCNT</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> <source>TCP_KEEPIDLE</source> + <target>TCP_KEEPIDLE</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> <source>TCP_KEEPINTVL</source> + <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> + <target>Направи снимка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> + <target>Докосни бутона </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> + <target>Докосни за активиране на профил.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> <source>Tap to join</source> + <target>Докосни за вход</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join incognito" xml:space="preserve"> <source>Tap to join incognito</source> + <target>Докосни за инкогнито вход</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to start a new chat" xml:space="preserve"> <source>Tap to start a new chat</source> + <target>Докосни за започване на нов чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> + <target>Тестът е неуспешен на стъпка %@.</target> <note>server test failure</note> </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> + <target>Тествай сървър</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Test servers" xml:space="preserve"> <source>Test servers</source> + <target>Тествай сървърите</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> + <target>Тестовете са неуспешни!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> + <target>Благодарим Ви, че инсталирахте SimpleX Chat!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Благодарение на потребителите – [допринесете през Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> <source>Thanks to the users – contribute via Weblate!</source> + <target>Благодарение на потребителите – допринесете през Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> <source>The 1st platform without any user identifiers – private by design.</source> + <target>Първата платформа без никакви потребителски идентификатори – поверителна по дизайн.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> + <target>Неправилно ID на следващото съобщение (по-малко или еднакво с предишното). +Това може да се случи поради някаква грешка или когато връзката е компрометирана.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> + <target>Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> + <target>Опитът за промяна на паролата на базата данни не беше завършен.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> + <target>Връзката, която приехте, ще бъде отказана!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> <source>The contact you shared this link with will NOT be able to connect!</source> + <target>Контактът, с когото споделихте този линк, НЯМА да може да се свърже!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> <source>The created archive is available via app Settings / Database / Old database archive.</source> + <target>Създаденият архив е достъпен чрез Настройки на приложението / База данни / Стар архив на база данни.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> <source>The group is fully decentralized – it is visible only to the members.</source> + <target>Групата е напълно децентрализирана – видима е само за членовете.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> + <target>Хешът на предишното съобщение е различен.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be deleted for all members." xml:space="preserve"> <source>The message will be deleted for all members.</source> + <target>Съобщението ще бъде изтрито за всички членове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> <source>The message will be marked as moderated for all members.</source> + <target>Съобщението ще бъде маркирано като модерирано за всички членове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The next generation of private messaging" xml:space="preserve"> <source>The next generation of private messaging</source> + <target>Ново поколение поверителни съобщения</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> + <target>Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> <source>The profile is only shared with your contacts.</source> + <target>Профилът се споделя само с вашите контакти.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> + <source>The second tick we missed! ✅</source> + <target>Втората отметка, която пропуснахме! ✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> <source>The sender will NOT be notified</source> + <target>Подателят НЯМА да бъде уведомен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> + <target>Сървърите за нови връзки на текущия ви чат профил **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Theme" xml:space="preserve"> <source>Theme</source> + <target>Тема</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="There should be at least one user profile." xml:space="preserve"> <source>There should be at least one user profile.</source> + <target>Трябва да има поне един потребителски профил.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> <source>There should be at least one visible user profile.</source> + <target>Трябва да има поне един видим потребителски профил.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> <source>These settings are for your current profile **%@**.</source> + <target>Тези настройки са за текущия ви профил **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact and group settings" xml:space="preserve"> - <source>They can be overridden in contact and group settings</source> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Те могат да бъдат променени в настройките за всеки контакт и група.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> + <target>Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> + <target>Това действие не може да бъде отменено - съобщенията, изпратени и получени по-рано от избраното, ще бъдат изтрити. Може да отнеме няколко минути.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> + <target>Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Тази група има над %lld членове, потвърждения за доставка не се изпращат.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> + <target>Тази група вече не съществува.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> + <target>Тази настройка се прилага за съобщения в текущия ви профил **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> + <target>За да задавате въпроси и да получавате актуализации:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> <source>To connect, your contact can scan QR code or use the link in the app.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> + <target>За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> + <target>За да направите нова връзка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <target>За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> <source>To protect timezone, image/voice files use UTC.</source> + <target>За да не се разкрива часовата зона, файловете с изображения/глас използват UTC.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> + <target>За да защитите информацията си, включете SimpleX заключване. +Ще бъдете подканени да извършите идентификация, преди тази функция да бъде активирана.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> + <target>За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> + <target>За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> + <target>За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> + <target>За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Избор на инкогнито при свързване.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> + <target>Транспортна изолация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> + <target>Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact.</source> + <target>Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> + <target>Изключи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn off notifications?" xml:space="preserve"> <source>Turn off notifications?</source> + <target>Изключи известията?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> + <target>Включи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unable to record voice message" xml:space="preserve"> <source>Unable to record voice message</source> + <target>Не може да се запише гласово съобщение</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> - <note>No comment provided by engineer.</note> + <target>Неочаквана грешка: %@</target> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> + <target>Неочаквано състояние на миграция</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> + <target>Премахни от любимите</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> + <target>Покажи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide chat profile" xml:space="preserve"> <source>Unhide chat profile</source> + <target>Покажи чат профила</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide profile" xml:space="preserve"> <source>Unhide profile</source> + <target>Покажи профила</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unit" xml:space="preserve"> <source>Unit</source> + <target>Мерна единица</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unknown caller" xml:space="preserve"> <source>Unknown caller</source> + <target>Неизвестен номер</target> <note>callkit banner</note> </trans-unit> <trans-unit id="Unknown database error: %@" xml:space="preserve"> <source>Unknown database error: %@</source> + <target>Неизвестна грешка в базата данни: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unknown error" xml:space="preserve"> <source>Unknown error</source> + <target>Непозната грешка</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> + <target>Освен ако не използвате интерфейса за повикване на iOS, активирайте режима "Не безпокой", за да избегнете прекъсвания.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> + <target>Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте. +За да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> <source>Unlock</source> + <target>Отключи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock app" xml:space="preserve"> <source>Unlock app</source> + <target>Отключи приложението</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> + <target>Уведомявай</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> + <target>Непрочетено</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> <source>Update</source> + <target>Актуализация</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> <source>Update .onion hosts setting?</source> + <target>Актуализиране на настройката за .onion хостове?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> + <target>Актуализирай паролата на базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update network settings?" xml:space="preserve"> <source>Update network settings?</source> + <target>Актуализиране на мрежовите настройки?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update transport isolation mode?" xml:space="preserve"> <source>Update transport isolation mode?</source> + <target>Актуализиране на режима на изолация на транспорта?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> <source>Updating settings will re-connect the client to all servers.</source> + <target>Актуализирането на настройките ще свърже отново клиента към всички сървъри.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> <source>Updating this setting will re-connect the client to all servers.</source> + <target>Актуализирането на тази настройка ще свърже повторно клиента към всички сървъри.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> + <target>Актуализирай и отвори чата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> + <target>Качи файл</target> <note>server test step</note> </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> + <target>Използвай .onion хостове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> + <target>Използвай сървърите на SimpleX Chat?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> + <target>Използвай чата</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Използвай текущия профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> + <target>Използвай за нови връзки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> <source>Use iOS call interface</source> + <target>Използвай интерфейса за повикване на iOS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Използвай нов инкогнито профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> + <target>Използвай сървър</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="User profile" xml:space="preserve"> <source>User profile</source> + <target>Потребителски профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> <source>Using .onion hosts requires compatible VPN provider.</source> + <target>Използването на .onion хостове изисква съвместим VPN доставчик.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> + <target>Използват се сървърите на SimpleX Chat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> <source>Verify connection security</source> + <target>Потвръди сигурността на връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> + <target>Потвръди кода за сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Via browser" xml:space="preserve"> <source>Via browser</source> + <target>Чрез браузър</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> <source>Video call</source> + <target>Видео разговор</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> + <target>Видеото ще бъде получено, когато вашият контакт завърши качването му.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Video will be received when your contact is online, please wait or check later!</source> + <target>Видеото ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> <source>Videos and files up to 1gb</source> + <target>Видео и файлове до 1gb</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> + <target>Виж кода за сигурност</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> + <target>Гласови съобщения</target> <note>chat feature</note> </trans-unit> <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> <source>Voice messages are prohibited in this chat.</source> + <target>Гласовите съобщения са забранени в този чат.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> <source>Voice messages are prohibited in this group.</source> + <target>Гласовите съобщения са забранени в тази група.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> + <target>Гласовите съобщения са забранени!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice message…" xml:space="preserve"> <source>Voice message…</source> + <target>Гласово съобщение…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> <source>Waiting for file</source> + <target>Изчаква се получаването на файла</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for image" xml:space="preserve"> <source>Waiting for image</source> + <target>Изчаква се получаването на изображението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for video" xml:space="preserve"> <source>Waiting for video</source> + <target>Изчаква се получаването на видеото</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> + <target>Предупреждение: Може да загубите някои данни!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="WebRTC ICE servers" xml:space="preserve"> <source>WebRTC ICE servers</source> + <target>WebRTC ICE сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Welcome %@!" xml:space="preserve"> <source>Welcome %@!</source> + <target>Добре дошли %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Welcome message" xml:space="preserve"> <source>Welcome message</source> + <target>Съобщение при посрещане</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> + <target>Какво е новото</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When available" xml:space="preserve"> <source>When available</source> + <target>Когато са налични</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> <source>When people request to connect, you can accept or reject it.</source> + <target>Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> + <target>Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> + <target>С незадължително съобщение при посрещане.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> + <target>Грешна парола за базата данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> + <target>Грешна парола!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="XFTP servers" xml:space="preserve"> <source>XFTP servers</source> + <target>XFTP сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You" xml:space="preserve"> <source>You</source> + <target>Вие</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> <source>You accepted connection</source> + <target>Вие приехте връзката</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You allow" xml:space="preserve"> <source>You allow</source> + <target>Вие позволявате</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> + <target>Вече имате чат профил със същото показвано име. Моля, изберете друго име.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> <source>You are already connected to %@.</source> + <target>Вече сте вече свързани с %@.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <source>You are connected to the server used to receive messages from this contact.</source> + <target>Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are invited to group" xml:space="preserve"> <source>You are invited to group</source> + <target>Поканени сте в групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> + <target>Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> + <target>Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона **Отваряне в мобилно приложение**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> <source>You can create it later</source> + <target>Можете да го създадете по-късно</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable later via Settings" xml:space="preserve"> + <source>You can enable later via Settings</source> + <target>Можете да активирате по-късно през Настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> <source>You can enable them later via app Privacy & Security settings.</source> + <target>Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> + <target>Можете да скриете или заглушите известията за потребителски профил - плъзнете надясно.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can now send messages to %@" xml:space="preserve"> <source>You can now send messages to %@</source> + <target>Вече можете да изпращате съобщения до %@</target> <note>notification body</note> </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> + <target>Можете да зададете визуализация на известията на заключен екран през настройките.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> + <target>Можете да споделите линк или QR код - всеки ще може да се присъедини към групата. Няма да загубите членовете на групата, ако по-късно я изтриете.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> <source>You can share this address with your contacts to let them connect with **%@**.</source> + <target>Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> <source>You can share your address as a link or QR code - anybody can connect to you.</source> + <target>Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> + <target>Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> + <target>Можете да включите SimpleX заключване през Настройки.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> <source>You can use markdown to format messages:</source> + <target>Можете да използвате markdown за форматиране на съобщенията:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> + <target>Не може да изпращате съобщения!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> + <target>Вие контролирате през кой сървър(и) **да получавате** съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> + <target>Не можахте да бъдете потвърдени; Моля, опитайте отново.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have no chats" xml:space="preserve"> <source>You have no chats</source> + <target>Нямате чатове</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> + <target>Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Вие поканихте контакта</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group" xml:space="preserve"> <source>You joined this group</source> + <target>Вие се присъединихте към тази група</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> <source>You joined this group. Connecting to inviting group member.</source> + <target>Вие се присъединихте към тази група. Свързване с поканващия член на групата.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> + <target>Трябва да използвате най-новата версия на вашата чат база данни САМО на едно устройство, в противен случай може да спрете да получавате съобщения от някои контакти.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> + <target>Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You rejected group invitation" xml:space="preserve"> <source>You rejected group invitation</source> + <target>Отхвърлихте поканата за групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You sent group invitation" xml:space="preserve"> <source>You sent group invitation</source> + <target>Изпратихте покана за групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> + <target>Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <source>You will be connected when your connection request is accepted, please wait or check later!</source> + <target>Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when your contact's device is online, please wait or check later!</source> + <target>Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> + <target>Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> <source>You will join a group this link refers to and connect to its group members.</source> + <target>Ще се присъедините към групата, към която този линк препраща, и ще се свържете с нейните членове.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <source>You will still receive calls and notifications from muted profiles when they are active.</source> + <target>Все още ще получавате обаждания и известия от заглушени профили, когато са активни.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> + <target>Ще спрете да получавате съобщения от тази група. Историята на чата ще бъде запазена.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> <source>You won't lose your contacts if you later delete your address.</source> + <target>Няма да загубите контактите си, ако по-късно изтриете адреса си.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> + <target>Опитвате се да поканите контакт, с когото сте споделили инкогнито профил, в групата, в която използвате основния си профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> + <target>Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your %@ servers" xml:space="preserve"> <source>Your %@ servers</source> + <target>Вашите %@ сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> + <target>Вашите ICE сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your SMP servers" xml:space="preserve"> <source>Your SMP servers</source> + <target>Вашите SMP сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> + <target>Вашият SimpleX адрес</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your XFTP servers" xml:space="preserve"> <source>Your XFTP servers</source> + <target>Вашите XFTP сървъри</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> + <target>Вашите обаждания</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database" xml:space="preserve"> <source>Your chat database</source> + <target>Вашата чат база данни</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> + <target>Вашата чат база данни не е криптирана - задайте парола, за да я криптирате.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> <source>Your chat profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> + <target>Вашият чат профил ще бъде изпратен на членовете на групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> + <target>Вашите чат профили</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> <source>Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link).</source> + <target>Вашият контакт трябва да бъде онлайн, за да осъществите връзката. +Можете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> + <target>Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> <source>Your contacts can allow full message deletion.</source> + <target>Вашите контакти могат да позволят пълното изтриване на съобщението.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> <source>Your contacts in SimpleX will see it. You can change it in Settings.</source> + <target>Вашите контакти в SimpleX ще го видят. +Можете да го промените в Настройки.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> + <target>Вашите контакти ще останат свързани.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> + <target>Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current profile" xml:space="preserve"> <source>Your current profile</source> + <target>Вашият текущ профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your preferences" xml:space="preserve"> <source>Your preferences</source> + <target>Вашите настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your privacy" xml:space="preserve"> <source>Your privacy</source> + <target>Вашата поверителност</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Вашият профил **%@** ще бъде споделен.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> + <target>Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. +SimpleX сървърите не могат да видят вашия профил.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> + <target>Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> + <target>Вашият автоматично генериран профил</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your server" xml:space="preserve"> <source>Your server</source> + <target>Вашият сървър</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> + <target>Вашият адрес на сървъра</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> + <target>Вашите настройки</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> + <target>[Допринеси](https://github.com/simplex-chat/simplex-chat#contribute)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> <source>[Send us email](mailto:chat@simplex.chat)</source> + <target>[Изпратете ни имейл](mailto:chat@simplex.chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> + <target>[Звезда в GitHub](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="_italic_" xml:space="preserve"> <source>\_italic_</source> + <target>\_курсив_</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="`a + b`" xml:space="preserve"> <source>\`a + b`</source> + <target>\`a + b`</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="above, then choose:" xml:space="preserve"> <source>above, then choose:</source> + <target>по-горе, след това избери:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="accepted call" xml:space="preserve"> <source>accepted call</source> + <target>обаждането прието</target> <note>call status</note> </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> + <target>админ</target> <note>member role</note> </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> + <target>съгласуване на криптиране за %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="agreeing encryption…" xml:space="preserve"> <source>agreeing encryption…</source> + <target>съгласуване на криптиране…</target> <note>chat item text</note> </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> + <target>винаги</target> <note>pref value</note> </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> + <target>аудио разговор (не е e2e криптиран)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> <source>bad message ID</source> + <target>лошо ID на съобщението</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="bad message hash" xml:space="preserve"> <source>bad message hash</source> + <target>лош хеш на съобщението</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> + <target>удебелен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> + <target>грешка при повикване</target> <note>call status</note> </trans-unit> <trans-unit id="call in progress" xml:space="preserve"> <source>call in progress</source> + <target>в момента тече разговор</target> <note>call status</note> </trans-unit> <trans-unit id="calling…" xml:space="preserve"> <source>calling…</source> + <target>повикване…</target> <note>call status</note> </trans-unit> <trans-unit id="cancelled %@" xml:space="preserve"> <source>cancelled %@</source> + <target>отменен %@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> + <target>променен е адреса за вас</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> <source>changed role of %1$@ to %2$@</source> + <target>променена роля от %1$@ на %2$@</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="changed your role to %@" xml:space="preserve"> <source>changed your role to %@</source> + <target>променена е вашата ролята на %@</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> <source>changing address for %@…</source> + <target>промяна на адреса за %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="changing address…" xml:space="preserve"> <source>changing address…</source> + <target>промяна на адреса…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> <source>colored</source> + <target>цветен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="complete" xml:space="preserve"> <source>complete</source> + <target>завършен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> <source>connect to SimpleX Chat developers.</source> + <target>свържете се с разработчиците на SimpleX Chat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connected" xml:space="preserve"> <source>connected</source> + <target>свързан</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> + <target>свързване</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (accepted)" xml:space="preserve"> <source>connecting (accepted)</source> + <target>свързване (прието)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (announced)" xml:space="preserve"> <source>connecting (announced)</source> + <target>свързване (обявено)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (introduced)" xml:space="preserve"> <source>connecting (introduced)</source> + <target>свързване (представен)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> <source>connecting (introduction invitation)</source> + <target>свързване (покана за представяне)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting call" xml:space="preserve"> <source>connecting call…</source> + <target>разговорът се свързва…</target> <note>call status</note> </trans-unit> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> + <target>свързване…</target> <note>chat list item title</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> + <target>установена е връзка</target> <note>chat list item title (it should not be shown</note> </trans-unit> <trans-unit id="connection:%@" xml:space="preserve"> <source>connection:%@</source> + <target>връзка:%@</target> <note>connection information</note> </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> + <target>контактът има e2e криптиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="contact has no e2e encryption" xml:space="preserve"> <source>contact has no e2e encryption</source> + <target>контактът няма e2e криптиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="creator" xml:space="preserve"> <source>creator</source> + <target>създател</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="custom" xml:space="preserve"> <source>custom</source> + <target>персонализиран</target> <note>dropdown time picker choice</note> </trans-unit> <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> <source>database version is newer than the app, but no down migration for: %@</source> + <target>версията на базата данни е по-нова от приложението, но няма миграция надолу за: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="days" xml:space="preserve"> <source>days</source> + <target>дни</target> <note>time unit</note> </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> + <target>по подразбиране (%@)</target> <note>pref value</note> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> + <target>по подразбиране (не)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="default (yes)" xml:space="preserve"> <source>default (yes)</source> + <target>по подразбиране (да)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="deleted" xml:space="preserve"> <source>deleted</source> + <target>изтрит</target> <note>deleted chat item</note> </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> <source>deleted group</source> + <target>групата изтрита</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> <source>different migration in the app/database: %@ / %@</source> + <target>различна миграция в приложението/базата данни: %@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="direct" xml:space="preserve"> <source>direct</source> + <target>директна</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>деактивирано</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> + <target>дублирано съобщение</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> + <target>e2e криптиран</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="enabled" xml:space="preserve"> <source>enabled</source> + <target>активирано</target> <note>enabled status</note> </trans-unit> <trans-unit id="enabled for contact" xml:space="preserve"> <source>enabled for contact</source> + <target>активирано за контакт</target> <note>enabled status</note> </trans-unit> <trans-unit id="enabled for you" xml:space="preserve"> <source>enabled for you</source> + <target>активирано за вас</target> <note>enabled status</note> </trans-unit> <trans-unit id="encryption agreed" xml:space="preserve"> <source>encryption agreed</source> + <target>криптирането е съгласувано</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption agreed for %@" xml:space="preserve"> <source>encryption agreed for %@</source> + <target>криптирането е съгласувано за %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok" xml:space="preserve"> <source>encryption ok</source> + <target>криптирането работи</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok for %@" xml:space="preserve"> <source>encryption ok for %@</source> + <target>криптирането работи за %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> <source>encryption re-negotiation allowed</source> + <target>разрешено повторно договаряне на криптиране</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> <source>encryption re-negotiation allowed for %@</source> + <target>разрешено повторно договаряне на криптиране за %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required" xml:space="preserve"> <source>encryption re-negotiation required</source> + <target>необходимо е повторно договаряне на криптиране</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> <source>encryption re-negotiation required for %@</source> + <target>необходимо е повторно договаряне на криптиране за %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="ended" xml:space="preserve"> <source>ended</source> + <target>приключен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="ended call %@" xml:space="preserve"> <source>ended call %@</source> + <target>приключи разговор %@</target> <note>call status</note> </trans-unit> <trans-unit id="error" xml:space="preserve"> <source>error</source> + <target>грешка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>събитие се случи</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> + <target>групата е изтрита</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group profile updated" xml:space="preserve"> <source>group profile updated</source> + <target>профилът на групата е актуализиран</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="hours" xml:space="preserve"> <source>hours</source> + <target>часове</target> <note>time unit</note> </trans-unit> <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> + <target>iOS Keychain се използва за сигурно съхраняване на парола - позволява получаване на push известия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> + <target>iOS Keychain ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на push известия.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> + <target>инкогнито чрез линк с адрес за контакт</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via group link" xml:space="preserve"> <source>incognito via group link</source> + <target>инкогнито чрез групов линк</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via one-time link" xml:space="preserve"> <source>incognito via one-time link</source> + <target>инкогнито чрез еднократен линк за връзка</target> <note>chat list item description</note> </trans-unit> <trans-unit id="indirect (%d)" xml:space="preserve"> <source>indirect (%d)</source> + <target>индиректна (%d)</target> <note>connection level description</note> </trans-unit> <trans-unit id="invalid chat" xml:space="preserve"> <source>invalid chat</source> + <target>невалиден чат</target> <note>invalid chat data</note> </trans-unit> <trans-unit id="invalid chat data" xml:space="preserve"> <source>invalid chat data</source> + <target>невалидни данни за чат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invalid data" xml:space="preserve"> <source>invalid data</source> + <target>невалидни данни</target> <note>invalid chat item</note> </trans-unit> <trans-unit id="invitation to group %@" xml:space="preserve"> <source>invitation to group %@</source> + <target>покана за група %@</target> <note>group name</note> </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> + <target>поканен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invited %@" xml:space="preserve"> <source>invited %@</source> + <target>поканен %@</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="invited to connect" xml:space="preserve"> <source>invited to connect</source> + <target>поканен да се свърже</target> <note>chat list item title</note> </trans-unit> <trans-unit id="invited via your group link" xml:space="preserve"> <source>invited via your group link</source> + <target>поканен чрез вашия групов линк</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="italic" xml:space="preserve"> <source>italic</source> + <target>курсив</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="join as %@" xml:space="preserve"> <source>join as %@</source> + <target>присъединяване като %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="left" xml:space="preserve"> <source>left</source> + <target>напусна</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="marked deleted" xml:space="preserve"> <source>marked deleted</source> + <target>маркирано като изтрито</target> <note>marked deleted chat item preview text</note> </trans-unit> <trans-unit id="member" xml:space="preserve"> <source>member</source> + <target>член</target> <note>member role</note> </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> + <target>свързан</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> + <target>получено съобщение</target> <note>notification</note> </trans-unit> <trans-unit id="minutes" xml:space="preserve"> <source>minutes</source> + <target>минути</target> <note>time unit</note> </trans-unit> <trans-unit id="missed call" xml:space="preserve"> <source>missed call</source> + <target>пропуснато повикване</target> <note>call status</note> </trans-unit> <trans-unit id="moderated" xml:space="preserve"> <source>moderated</source> + <target>модерирано</target> <note>moderated chat item</note> </trans-unit> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> + <target>модерирано от %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="months" xml:space="preserve"> <source>months</source> + <target>месеци</target> <note>time unit</note> </trans-unit> <trans-unit id="never" xml:space="preserve"> <source>never</source> + <target>никога</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> + <target>ново съобщение</target> <note>notification</note> </trans-unit> <trans-unit id="no" xml:space="preserve"> <source>no</source> + <target>не</target> <note>pref value</note> </trans-unit> <trans-unit id="no e2e encryption" xml:space="preserve"> <source>no e2e encryption</source> + <target>липсва e2e криптиране</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="no text" xml:space="preserve"> <source>no text</source> + <target>няма текст</target> <note>copied message info in history</note> </trans-unit> <trans-unit id="observer" xml:space="preserve"> <source>observer</source> + <target>наблюдател</target> <note>member role</note> </trans-unit> <trans-unit id="off" xml:space="preserve"> <source>off</source> + <target>изключено</target> <note>enabled status group pref value</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> + <target>предлага %@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="offered %@: %@" xml:space="preserve"> <source>offered %1$@: %2$@</source> + <target>предлага %1$@: %2$@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="on" xml:space="preserve"> <source>on</source> + <target>включено</target> <note>group pref value</note> </trans-unit> <trans-unit id="or chat with the developers" xml:space="preserve"> <source>or chat with the developers</source> + <target>или пишете на разработчиците</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> + <target>собственик</target> <note>member role</note> </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> + <target>peer-to-peer</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> + <target>получен отговор…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="received confirmation…" xml:space="preserve"> <source>received confirmation…</source> + <target>получено потвърждение…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> + <target>отхвърлено повикване</target> <note>call status</note> </trans-unit> <trans-unit id="removed" xml:space="preserve"> <source>removed</source> + <target>отстранен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="removed %@" xml:space="preserve"> <source>removed %@</source> + <target>отстранен %@</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> + <target>ви острани</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> + <target>сек.</target> <note>network option</note> </trans-unit> <trans-unit id="seconds" xml:space="preserve"> <source>seconds</source> + <target>секунди</target> <note>time unit</note> </trans-unit> <trans-unit id="secret" xml:space="preserve"> <source>secret</source> + <target>таен</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="security code changed" xml:space="preserve"> <source>security code changed</source> + <target>кодът за сигурност е променен</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> + <target>стартиране…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="strike" xml:space="preserve"> <source>strike</source> + <target>зачеркнат</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="this contact" xml:space="preserve"> <source>this contact</source> + <target>този контакт</target> <note>notification title</note> </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> + <target>неизвестен</target> <note>connection info</note> </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> + <target>актуализиран профил на групата</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> <source>v%@ (%@)</source> + <target>v%@ (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="via contact address link" xml:space="preserve"> <source>via contact address link</source> + <target>чрез линк с адрес за контакт</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via group link" xml:space="preserve"> <source>via group link</source> + <target>чрез групов линк</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via one-time link" xml:space="preserve"> <source>via one-time link</source> + <target>чрез еднократен линк за връзка</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via relay" xml:space="preserve"> <source>via relay</source> + <target>чрез реле</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> + <target>видео разговор (не е e2e криптиран)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="waiting for answer…" xml:space="preserve"> <source>waiting for answer…</source> + <target>чака се отговор…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="waiting for confirmation…" xml:space="preserve"> <source>waiting for confirmation…</source> + <target>чака се за потвърждение…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="wants to connect to you!" xml:space="preserve"> <source>wants to connect to you!</source> + <target>иска да се свърже с вас!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="weeks" xml:space="preserve"> <source>weeks</source> + <target>седмици</target> <note>time unit</note> </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> + <target>да</target> <note>pref value</note> </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> + <target>вие сте поканени в групата</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="you are observer" xml:space="preserve"> <source>you are observer</source> + <target>вие сте наблюдател</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> + <target>променихте адреса</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed address for %@" xml:space="preserve"> <source>you changed address for %@</source> + <target>променихте адреса за %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> <source>you changed role for yourself to %@</source> + <target>променихте ролята си на %@</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> <source>you changed role of %1$@ to %2$@</source> + <target>променихте ролята на %1$@ на %2$@</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you left" xml:space="preserve"> <source>you left</source> + <target>вие напуснахте</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you removed %@" xml:space="preserve"> <source>you removed %@</source> + <target>премахнахте %@</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you shared one-time link" xml:space="preserve"> <source>you shared one-time link</source> + <target>споделихте еднократен линк за връзка</target> <note>chat list item description</note> </trans-unit> <trans-unit id="you shared one-time link incognito" xml:space="preserve"> <source>you shared one-time link incognito</source> + <target>споделихте еднократен инкогнито линк за връзка</target> <note>chat list item description</note> </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> + <target>вие: </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="~strike~" xml:space="preserve"> <source>\~strike~</source> + <target>\~зачеркнат~</target> <note>No comment provided by engineer.</note> </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX</source> + <target>SimpleX</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> + <target>SimpleX се нуждае от достъп до камерата, за да сканира QR кодове, за да се свърже с други потребители и за видео разговори.</target> <note>Privacy - Camera Usage Description</note> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> + <target>SimpleX използва Face ID за локалнa идентификация</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> + <target>SimpleX се нуждае от достъп до микрофона за аудио и видео разговори и за запис на гласови съобщения.</target> <note>Privacy - Microphone Usage Description</note> </trans-unit> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> + <target>SimpleX се нуждае от достъп до фотобиблиотека за запазване на заснета и получена медия</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="bg" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle display name</note> </trans-unit> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> + <target>Авторско право © 2022 SimpleX Chat. Всички права запазени.</target> <note>Copyright (human-readable)</note> </trans-unit> </body> diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..124ddbcc33 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cf485752ea --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,30 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..3af673b19f --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,10 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json new file mode 100644 index 0000000000..23e8239ce8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "bg", + "toolInfo" : { + "toolBuildNumber" : "15A240d", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "15.0" + }, + "version" : "1.0" +} \ No newline at end of file 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 cf658cc7e1..bbfd95e6bf 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="cs" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 barevný!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Historie</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## Odpovídáno</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#tajný#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ a %@ připojen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ na %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ se chce připojit!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ a %lld ostatní členové připojeni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,10 @@ <target>%lld minut</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld vteřin</target> @@ -302,6 +331,12 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +432,9 @@ <target>Nový kontakt</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Náhodný profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Vašemu kontaktu bude zaslán náhodný profil</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Nový náhodný profil bude sdílen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +490,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Přijmout kontakt</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Přijmout kontakt?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +503,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Přijmout inkognito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -680,6 +710,10 @@ <target>Sestavení aplikace: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Ikona aplikace</target> @@ -815,6 +849,10 @@ <target>Hlasové zprávy můžete posílat vy i váš kontakt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Podle chat profilu (výchozí) nebo [podle připojení](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -1046,9 +1084,19 @@ <target>Připojit</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Připojit se přes kontaktní odkaz?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Připojit přímo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Spojit se inkognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Připojit se přes odkaz</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1114,9 @@ <target>Připojit se prostřednictvím odkazu / QR kódu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Připojit se jednorázovým odkazem?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Připojit se jednorázovým odkazem</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1144,6 @@ <target>Chyba spojení (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Žádost o připojení</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Požadavek na připojení byl odeslán!</target> @@ -1206,6 +1249,10 @@ <target>Vytvořit odkaz</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Vytvořit jednorázovou pozvánku</target> @@ -1549,6 +1596,11 @@ <target>Smazáno v: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Doručenka</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Potvrzení o doručení jsou vypnuté!</target> @@ -1654,6 +1706,10 @@ <target>Odpojit</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Zobrazované jméno</target> @@ -1789,6 +1845,14 @@ <target>Šifrovat databázi?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Zašifrovaná databáze</target> @@ -1914,11 +1978,19 @@ <target>Chyba při vytváření odkazu skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Chyba při vytváření profilu!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Chyba při mazání databáze chatu</target> @@ -2039,6 +2111,10 @@ <target>Chyba odesílání e-mailu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Chyba při odesílání zprávy</target> @@ -2046,6 +2122,7 @@ </trans-unit> <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> <source>Error setting delivery receipts!</source> + <target>Chyba nastavování potvrzení o doručení!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> @@ -2255,7 +2332,7 @@ </trans-unit> <trans-unit id="Full name (optional)" xml:space="preserve"> <source>Full name (optional)</source> - <target>Celé jméno (volitelné)</target> + <target>Celé jméno (volitelně)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full name:" xml:space="preserve"> @@ -2436,7 +2513,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Historie</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2546,7 +2623,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>V odpovědi na</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2558,14 +2635,9 @@ <target>Režim inkognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Zde není podporován režim inkognito - členům skupiny bude zaslán váš hlavní profil</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>Režim inkognito chrání soukromí vašeho hlavního profilového jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2640,6 +2712,11 @@ <target>Neplatná adresa serveru!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Neplatný status</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Platnost pozvánky vypršela!</target> @@ -2723,12 +2800,12 @@ </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> <source>Join incognito</source> - <target>Připojte se inkognito</target> + <target>Připojit se inkognito</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> <source>Joining group</source> - <target>Připojení ke skupině</target> + <target>Připojování ke skupině</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> @@ -2899,7 +2976,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Chyba doručení zprávy</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2986,6 +3063,11 @@ <target>Další vylepšení se chystají již brzy!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Pravděpodobně je toto spojení smazáno.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Tento kontakt s největší pravděpodobností smazal spojení s vámi.</target> @@ -3046,6 +3128,10 @@ <target>Archiv nové databáze</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Nově zobrazované jméno</target> @@ -3091,6 +3177,10 @@ <target>Žádné kontakty k přidání</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Žádný token zařízení!</target> @@ -3255,6 +3345,10 @@ <target>Hlasové zprávy může odesílat pouze váš kontakt.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Otevřít nastavení</target> @@ -3345,10 +3439,10 @@ <target>Vložení přijatého odkazu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3547,6 +3641,7 @@ </trans-unit> <trans-unit id="Protocol timeout per KB" xml:space="preserve"> <source>Protocol timeout per KB</source> + <target>Časový limit protokolu na KB</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> @@ -3561,6 +3656,7 @@ </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> + <target>Reagovat…</target> <note>chat item menu</note> </trans-unit> <trans-unit id="Read" xml:space="preserve"> @@ -3593,6 +3689,10 @@ <target>Přečtěte si více v našem [GitHub repozitáři](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Přijato v</target> @@ -3615,6 +3715,7 @@ </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving file will be stopped." xml:space="preserve"> @@ -3634,10 +3735,12 @@ </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>Znovu připojte všechny připojené servery a vynuťte doručení zprávy. Využívá další provoz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> + <target>Znovu připojit servery?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> @@ -3660,8 +3763,8 @@ <target>Odmítnout</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Odmítnout kontakt (odesílatel NEBUDE upozorněn)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3972,6 +4075,7 @@ </trans-unit> <trans-unit id="Send delivery receipts to" xml:space="preserve"> <source>Send delivery receipts to</source> + <target>Potvrzení o doručení zasílat na</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send direct message" xml:space="preserve"> @@ -3979,6 +4083,10 @@ <target>Odeslat přímou zprávu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Poslat mizící zprávu</target> @@ -4049,11 +4157,19 @@ <target>Odesílání potvrzení o doručení je vypnuto pro %lld kontakty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>Odesílání potvrzení o doručení je povoleno pro %lld kontakty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Odesílání přes</target> @@ -4194,6 +4310,10 @@ <target>Zobrazit možnosti vývojáře</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Zobrazení náhledu</target> @@ -4264,6 +4384,10 @@ <target>Jednorázová pozvánka SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Přeskočit</target> @@ -4274,6 +4398,10 @@ <target>Přeskočené zprávy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli.</target> @@ -4533,6 +4661,7 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> <source>The second tick we missed! ✅</source> + <target>Druhé zaškrtnutí jsme přehlédli! ✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -4565,9 +4694,9 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Toto nastavení je pro váš aktuální profil **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Mohou být přepsány v nastavení kontaktů</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Mohou být přepsány v nastavení kontaktů.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4585,6 +4714,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Tuto akci nelze vzít zpět - váš profil, kontakty, zprávy a soubory budou nenávratně ztraceny.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Tato skupina již neexistuje.</target> @@ -4605,11 +4738,6 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován <target>Pro připojení může váš kontakt naskenovat QR kód, nebo použít odkaz v aplikaci.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části chatu.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Vytvoření nového připojení</target> @@ -4652,6 +4780,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <target>Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Izolace transportu</target> @@ -4690,7 +4822,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření.</target> <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Neočekávaná chyba: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4829,6 +4961,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Použijte chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Použít pro nová připojení</target> @@ -4839,6 +4975,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Použít rozhraní volání iOS</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Použít server</target> @@ -5129,8 +5269,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>Pozvali jste svůj kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5259,11 +5399,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu <target>Váš chat profil bude zaslán členům skupiny</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Váš chat profil bude odeslán vašemu kontaktu</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Vaše chat profily</target> @@ -5318,6 +5453,10 @@ Můžete ji změnit v Nastavení.</target> <target>Vaše soukromí</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5325,11 +5464,6 @@ SimpleX servers cannot see your profile.</source> Servery SimpleX nevidí váš profil.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Váš profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení.</target> @@ -5495,6 +5629,10 @@ Servery SimpleX nevidí váš profil.</target> <target>připojeno</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>připojování</target> @@ -5577,10 +5715,12 @@ Servery SimpleX nevidí váš profil.</target> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> + <target>výchozí (ne)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="default (yes)" xml:space="preserve"> <source>default (yes)</source> + <target>výchozí (ano)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="deleted" xml:space="preserve"> @@ -5603,6 +5743,10 @@ Servery SimpleX nevidí váš profil.</target> <target>přímo</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>duplicitní zpráva</target> @@ -5683,6 +5827,10 @@ Servery SimpleX nevidí váš profil.</target> <target>chyba</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>skupina smazána</target> @@ -5944,6 +6092,10 @@ Servery SimpleX nevidí váš profil.</target> <target>bezpečnostní kód změněn</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>začíná…</target> @@ -6088,7 +6240,7 @@ Servery SimpleX nevidí váš profil.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6120,7 +6272,7 @@ Servery SimpleX nevidí váš profil.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="cs" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json index 6b3a1afe65..5c7c929ee3 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "cs", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 56a34d095f..114b7f3e73 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="de" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 farbig!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Vergangenheit</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## Als Antwort auf</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#geheim#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ und %@ wurden verbunden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ an %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ will sich mit Ihnen verbinden!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ und %lld weitere Mitglieder wurden verbunden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,10 @@ <target>%lld Minuten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld Sekunde(n)</target> @@ -302,6 +331,12 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +432,9 @@ <target>Ein neuer Kontakt</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Ein zufälliges Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Ein zufälliges Profil wird an Ihren Kontakt gesendet</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Es wird ein neues Zufallsprofil geteilt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +490,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Kontakt annehmen</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Kontaktanfrage annehmen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +503,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Inkognito akzeptieren</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -542,7 +572,7 @@ </trans-unit> <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> - <target>Alle Daten werden gelöscht, sobald diese eingegeben wird.</target> + <target>Alle Daten werden gelöscht, sobald dieser eingegeben wird.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> @@ -572,12 +602,12 @@ </trans-unit> <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> <source>Allow calls only if your contact allows them.</source> - <target>Anrufe sind nur erlaubt, wenn Ihr Kontakt das ebenfalls erlaubt.</target> + <target>Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> - <target>Verschwindende Nachrichten nur erlauben, wenn Ihr Kontakt das ebenfalls erlaubt.</target> + <target>Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> @@ -587,7 +617,7 @@ </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> <source>Allow message reactions only if your contact allows them.</source> - <target>Reaktionen auf Nachrichten sind nur möglich, falls Ihr Kontakt dies erlaubt.</target> + <target>Erlauben Sie Reaktionen auf Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions." xml:space="preserve"> @@ -622,7 +652,7 @@ </trans-unit> <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> <source>Allow voice messages only if your contact allows them.</source> - <target>Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.</target> + <target>Erlauben Sie Sprachnachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages?" xml:space="preserve"> @@ -680,9 +710,13 @@ <target>App Build: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> - <target>App Icon</target> + <target>App-Icon</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App passcode" xml:space="preserve"> @@ -727,12 +761,12 @@ </trans-unit> <trans-unit id="Audio/video calls" xml:space="preserve"> <source>Audio/video calls</source> - <target>Audio/Video Anrufe</target> + <target>Audio-/Video-Anrufe</target> <note>chat feature</note> </trans-unit> <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> <source>Audio/video calls are prohibited.</source> - <target>Audio/Video Anrufe sind nicht erlaubt.</target> + <target>Audio-/Video-Anrufe sind nicht erlaubt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication cancelled" xml:space="preserve"> @@ -815,6 +849,10 @@ <target>Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten senden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -948,7 +986,7 @@ </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> - <target>Chat Präferenzen</target> + <target>Chat-Präferenzen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chats" xml:space="preserve"> @@ -1046,9 +1084,19 @@ <target>Verbinden</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Über den Kontakt-Link verbinden?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Direkt verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Inkognito verbinden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Über den Kontakt-Link verbinden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1114,9 @@ <target>Über einen Link / QR-Code verbinden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Über einen Einmal-Link verbinden?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Über einen Einmal-Link verbinden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1144,6 @@ <target>Verbindungsfehler (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Verbindungsanfrage</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Verbindungsanfrage wurde gesendet!</target> @@ -1148,7 +1191,7 @@ </trans-unit> <trans-unit id="Contact preferences" xml:space="preserve"> <source>Contact preferences</source> - <target>Kontakt Präferenzen</target> + <target>Kontakt-Präferenzen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> @@ -1206,9 +1249,13 @@ <target>Link erzeugen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> - <target>Erstellen Sie einen einmaligen Einladungslink</target> + <target>Einmal-Einladungslink erstellen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> @@ -1549,14 +1596,19 @@ <target>Gelöscht um: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Zustellung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> - <target>Zustellungs-Quittierungen sind deaktiviert!</target> + <target>Empfangsbestätigungen sind deaktiviert!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> <source>Delivery receipts!</source> - <target>Zustellungs-Quittierungen!</target> + <target>Empfangsbestätigungen!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Description" xml:space="preserve"> @@ -1581,12 +1633,12 @@ </trans-unit> <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> - <target>Die Geräteauthentifizierung ist deaktiviert. SimpleX Sperre ist abgeschaltet.</target> + <target>Die Geräteauthentifizierung ist deaktiviert. SimpleX-Sperre ist abgeschaltet.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> - <target>Die Geräteauthentifizierung ist deaktiviert. Sie können die SimpleX Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</target> + <target>Die Geräteauthentifizierung ist deaktiviert. Sie können die SimpleX-Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> @@ -1611,7 +1663,7 @@ </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> <source>Disable SimpleX Lock</source> - <target>SimpleX Sperre deaktivieren</target> + <target>SimpleX-Sperre deaktivieren</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> @@ -1654,6 +1706,10 @@ <target>Trennen</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Angezeigter Name</target> @@ -1731,7 +1787,7 @@ </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> - <target>SimpleX Sperre aktivieren</target> + <target>SimpleX-Sperre aktivieren</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> @@ -1789,6 +1845,15 @@ <target>Datenbank verschlüsseln?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Lokale Dateien verschlüsseln</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Verschlüsselte Datenbank</target> @@ -1914,11 +1979,20 @@ <target>Fehler beim Erzeugen des Gruppen-Links</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Fehler beim Erstellen des Profils!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Fehler beim Entschlüsseln der Datei</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Fehler beim Löschen der Chat-Datenbank</target> @@ -1961,7 +2035,7 @@ </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> - <target>Fehler beim Aktivieren der Empfangsbestätigungen!</target> + <target>Fehler beim Aktivieren von Empfangsbestätigungen!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error enabling notifications" xml:space="preserve"> @@ -2039,6 +2113,10 @@ <target>Fehler beim Senden der eMail</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Fehler beim Senden der Nachricht</target> @@ -2046,7 +2124,7 @@ </trans-unit> <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> <source>Error setting delivery receipts!</source> - <target>Fehler beim Setzen der Empfangsbestätigungen!</target> + <target>Fehler beim Setzen von Empfangsbestätigungen!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> @@ -2311,7 +2389,7 @@ </trans-unit> <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> <source>Group invitation is no longer valid, it was removed by sender.</source> - <target>Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.</target> + <target>Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group link" xml:space="preserve"> @@ -2366,7 +2444,7 @@ </trans-unit> <trans-unit id="Group preferences" xml:space="preserve"> <source>Group preferences</source> - <target>Gruppenpräferenzen</target> + <target>Gruppen-Präferenzen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile" xml:space="preserve"> @@ -2437,7 +2515,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Vergangenheit</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2625,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>Als Antwort auf</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2556,17 +2634,12 @@ </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> <source>Incognito mode</source> - <target>Inkognito Modus</target> + <target>Inkognito-Modus</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>Der Inkognito-Modus schützt die Privatsphäre Ihres Hauptprofilnamens und -bildes – für jeden neuen Kontakt wird ein neues Zufallsprofil erstellt.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Der Inkognito-Modus schützt Ihre Privatsphäre, indem für jeden Kontakt ein neues Zufallsprofil erstellt wird.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2714,11 @@ <target>Ungültige Serveradresse!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Ungültiger Status</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Einladung abgelaufen!</target> @@ -2900,7 +2978,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Fehler bei der Nachrichtenzustellung</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3065,11 @@ <target>Weitere Verbesserungen sind bald verfügbar!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Wahrscheinlich ist diese Verbindung gelöscht worden.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht.</target> @@ -3047,6 +3130,10 @@ <target>Neues Datenbankarchiv</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Neuer Anzeigename</target> @@ -3092,6 +3179,11 @@ <target>Keine Kontakte zum Hinzufügen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Keine Information über die Zustellung</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Kein Geräte-Token!</target> @@ -3193,7 +3285,7 @@ </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> - <target>Gruppenpräferenzen können nur von Gruppen-Eigentümern geändert werden.</target> + <target>Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> @@ -3256,6 +3348,10 @@ <target>Nur Ihr Kontakt kann Sprachnachrichten versenden.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Geräte-Einstellungen öffnen</target> @@ -3293,7 +3389,7 @@ </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> - <target>PING Zähler</target> + <target>PING-Zähler</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="PING interval" xml:space="preserve"> @@ -3346,10 +3442,10 @@ <target>Fügen Sie den erhaltenen Link ein</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Um sich mit Ihrem Kontakt zu verbinden, fügen Sie den erhaltenen Link in das Feld unten ein.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3493,7 +3589,7 @@ </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> - <target>Audio/Video Anrufe nicht erlauben.</target> + <target>Audio-/Video-Anrufe nicht erlauben.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> @@ -3596,6 +3692,11 @@ <target>Erfahren Sie in unserem [GitHub-Repository](https://github.com/simplex-chat/simplex-chat#readme) mehr dazu.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Bestätigungen sind deaktiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Empfangen um</target> @@ -3666,8 +3767,8 @@ <target>Ablehnen</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Kontakt ablehnen (der Absender wird NICHT benachrichtigt)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3978,7 +4079,7 @@ </trans-unit> <trans-unit id="Send delivery receipts to" xml:space="preserve"> <source>Send delivery receipts to</source> - <target>Zustellungs-Quittierungen versenden an</target> + <target>Empfangsbestätigungen senden an</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send direct message" xml:space="preserve"> @@ -3986,6 +4087,10 @@ <target>Direktnachricht senden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Verschwindende Nachricht senden</target> @@ -4018,7 +4123,7 @@ </trans-unit> <trans-unit id="Send receipts" xml:space="preserve"> <source>Send receipts</source> - <target>Quittierungen versenden</target> + <target>Bestätigungen senden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> @@ -4053,12 +4158,22 @@ </trans-unit> <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is disabled for %lld contacts</source> - <target>Das Senden von Empfangsbestätigungen an %lld Kontakte ist deaktiviert</target> + <target>Sendebestätigungen sind für %lld Kontakte deaktiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Sendebestätigungen sind für %lld Gruppen deaktiviert</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> - <target>Das Senden von Empfangsbestätigungen an %lld Kontakte ist aktiviert</target> + <target>Sendebestätigungen sind für %lld Kontakte aktiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Sendebestätigungen sind für %lld Gruppen aktiviert</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> @@ -4123,7 +4238,7 @@ </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> - <target>Gruppenpräferenzen einstellen</target> + <target>Gruppen-Präferenzen einstellen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set it instead of system authentication." xml:space="preserve"> @@ -4201,6 +4316,11 @@ <target>Entwickleroptionen anzeigen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Letzte Nachrichten anzeigen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Vorschau anzeigen</target> @@ -4223,7 +4343,7 @@ </trans-unit> <trans-unit id="SimpleX Lock" xml:space="preserve"> <source>SimpleX Lock</source> - <target>SimpleX Sperre</target> + <target>SimpleX-Sperre</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock mode" xml:space="preserve"> @@ -4233,12 +4353,12 @@ </trans-unit> <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> <source>SimpleX Lock not enabled!</source> - <target>SimpleX Sperre ist nicht aktiviert!</target> + <target>SimpleX-Sperre ist nicht aktiviert!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> <source>SimpleX Lock turned on</source> - <target>SimpleX Sperre aktiviert</target> + <target>SimpleX-Sperre aktiviert</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX address" xml:space="preserve"> @@ -4248,7 +4368,7 @@ </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> - <target>SimpleX Kontaktadressen-Link</target> + <target>SimpleX-Kontaktadressen-Link</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> @@ -4268,9 +4388,13 @@ </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> <source>SimpleX one-time invitation</source> - <target>SimpleX Einmal-Link</target> + <target>SimpleX-Einmal-Einladung</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Überspringen</target> @@ -4281,6 +4405,11 @@ <target>Übersprungene Nachrichten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Kleine Gruppen (max. 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - in der Chat-Konsole finden Sie weitere Einzelheiten.</target> @@ -4540,7 +4669,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> <source>The second tick we missed! ✅</source> - <target>Das zweite Häkchen, welches wir vermisst haben! ✅</target> + <target>Wir haben das zweite Häkchen vermisst! ✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -4573,9 +4702,9 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Diese Einstellungen betreffen Ihr aktuelles Profil **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Diese können in den Kontakteinstellungen überschrieben werden</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Sie können in den Kontakteinstellungen überschrieben werden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4722,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %lld Mitglieder hat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Diese Gruppe existiert nicht mehr.</target> @@ -4613,11 +4747,6 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <target>Um eine Verbindung herzustellen, kann Ihr Kontakt den QR-Code scannen oder den Link in der App verwenden.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Um das für eine Inkognito-Verbindung verwendete Profil zu finden, tippen Sie oben im Chat auf den Kontakt- oder Gruppennamen.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Um eine Verbindung mit einem neuen Kontakt zu erstellen</target> @@ -4636,7 +4765,7 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> - <target>Um Ihre Informationen zu schützen, schalten Sie die SimpleX Sperre ein. + <target>Um Ihre Informationen zu schützen, schalten Sie die SimpleX-Sperre ein. Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -4660,6 +4789,10 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <target>Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Transport-Isolation</target> @@ -4698,7 +4831,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Unerwarteter Fehler: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4829,7 +4962,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> - <target>Verwenden Sie SimpleX Chat Server?</target> + <target>Verwenden Sie SimpleX-Chat-Server?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> @@ -4837,6 +4970,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Verwenden Sie Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Nutzen Sie das aktuelle Profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Für neue Verbindungen nutzen</target> @@ -4847,6 +4985,11 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>iOS Anrufschnittstelle nutzen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Nutzen Sie das neue Inkognito-Profil</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Server nutzen</target> @@ -4864,7 +5007,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> - <target>Verwende SimpleX Chat Server.</target> + <target>Verwendung von SimpleX-Chat-Servern.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> @@ -5104,7 +5247,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s </trans-unit> <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> - <target>Sie können die SimpleX Sperre über die Einstellungen aktivieren.</target> + <target>Sie können die SimpleX-Sperre über die Einstellungen aktivieren.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> @@ -5137,8 +5280,8 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>Sie haben Ihren Kontakt eingeladen</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5267,11 +5410,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s <target>Ihr Chat-Profil wird an Gruppenmitglieder gesendet</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Ihr Chat-Profil wird an Ihren Kontakt gesendet</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Meine Chat-Profile</target> @@ -5326,6 +5464,11 @@ Sie können es in den Einstellungen ändern.</target> <target>Meine Privatsphäre</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Ihr Profil **%@** wird geteilt.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5333,11 +5476,6 @@ SimpleX servers cannot see your profile.</source> SimpleX-Server können Ihr Profil nicht einsehen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</target> @@ -5495,7 +5633,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> <source>connect to SimpleX Chat developers.</source> - <target>Mit den SimpleX Chat Entwicklern verbinden.</target> + <target>Mit den SimpleX Chat-Entwicklern verbinden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connected" xml:space="preserve"> @@ -5503,6 +5641,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Verbunden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>verbinde</target> @@ -5525,7 +5667,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </trans-unit> <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> <source>connecting (introduction invitation)</source> - <target>Verbindung (eingeladen)</target> + <target>Verbinde (nach einer Einladung)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting call" xml:space="preserve"> @@ -5613,6 +5755,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>direkt</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>deaktiviert</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>Doppelte Nachricht</target> @@ -5693,6 +5840,11 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Fehler</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>event happened</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>Gruppe gelöscht</target> @@ -5954,6 +6106,10 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> <target>Sicherheitscode wurde geändert</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>Verbindung wird gestartet…</target> @@ -6098,7 +6254,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6130,7 +6286,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="de" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/de.xcloc/contents.json b/apps/ios/SimpleX Localizations/de.xcloc/contents.json index 3fa1dc4e8a..11924b71f5 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/de.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "de", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 19a0748afd..0aeeecfbe6 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="en" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 colored!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## History</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## In reply to</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#secret#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ and %@ connected</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ at %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ wants to connect!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ and %lld other members connected</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,11 @@ <target>%lld minutes</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld new interface languages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld second(s)</target> @@ -302,6 +332,15 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +436,9 @@ <target>A new contact</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>A random profile will be sent to the contact that you received this link from</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>A random profile will be sent to your contact</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>A new random profile will be shared.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +494,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Accept contact</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Accept connection request?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +507,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accept incognito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -680,6 +714,11 @@ <target>App build: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>App encrypts new local files (except videos).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>App icon</target> @@ -815,6 +854,11 @@ <target>Both you and your contact can send voice messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -1046,9 +1090,19 @@ <target>Connect</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Connect via contact link?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Connect directly</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Connect incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Connect via contact link</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1120,9 @@ <target>Connect via link / QR code</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Connect via one-time link?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Connect via one-time link</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1150,6 @@ <target>Connection error (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Connection request</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Connection request sent!</target> @@ -1206,6 +1255,11 @@ <target>Create link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Create one-time invitation link</target> @@ -1549,6 +1603,11 @@ <target>Deleted at: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Delivery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Delivery receipts are disabled!</target> @@ -1654,6 +1713,11 @@ <target>Disconnect</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Discover and join groups</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Display name</target> @@ -1789,6 +1853,16 @@ <target>Encrypt database?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Encrypt local files</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Encrypt stored files & media</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Encrypted database</target> @@ -1914,11 +1988,21 @@ <target>Error creating group link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <target>Error creating member contact</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Error creating profile!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Error decrypting file</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Error deleting chat database</target> @@ -2039,6 +2123,11 @@ <target>Error sending email</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <target>Error sending member contact invitation</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Error sending message</target> @@ -2437,7 +2526,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>History</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2636,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>In reply to</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2648,9 @@ <target>Incognito mode</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Incognito mode is not supported here - your main profile will be sent to group members</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Incognito mode protects your privacy by using a new random profile for each contact.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2725,11 @@ <target>Invalid server address!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Invalid status</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Invitation expired!</target> @@ -2900,7 +2989,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Message delivery error</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3076,11 @@ <target>More improvements are coming soon!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Most likely this connection is deleted.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Most likely this contact has deleted the connection with you.</target> @@ -3047,6 +3141,11 @@ <target>New database archive</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>New desktop app!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>New display name</target> @@ -3092,6 +3191,11 @@ <target>No contacts to add</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>No delivery information</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>No device token!</target> @@ -3256,6 +3360,11 @@ <target>Only your contact can send voice messages.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <target>Open</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Open Settings</target> @@ -3346,10 +3455,10 @@ <target>Paste received link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <target>Paste the link you received into the box below to connect with your contact.</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> + <target>Paste the link you received to connect with your contact.</target> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3596,6 +3705,11 @@ <target>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Receipts are disabled</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Received at</target> @@ -3666,9 +3780,9 @@ <target>Reject</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> - <target>Reject contact (sender NOT notified)</target> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> + <target>Reject (sender NOT notified)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject contact request" xml:space="preserve"> @@ -3986,6 +4100,11 @@ <target>Send direct message</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <target>Send direct message to connect</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Send disappearing message</target> @@ -4056,11 +4175,21 @@ <target>Sending receipts is disabled for %lld contacts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Sending receipts is disabled for %lld groups</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>Sending receipts is enabled for %lld contacts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Sending receipts is enabled for %lld groups</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Sending via</target> @@ -4201,6 +4330,11 @@ <target>Show developer options</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Show last messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Show preview</target> @@ -4271,6 +4405,11 @@ <target>SimpleX one-time invitation</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Simplified incognito mode</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Skip</target> @@ -4281,6 +4420,11 @@ <target>Skipped messages</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Small groups (max 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Some non-fatal errors occurred during import - you may see Chat console for more details.</target> @@ -4573,9 +4717,9 @@ It can happen because of some bug or when the connection is compromised.</target <target>These settings are for your current profile **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>They can be overridden in contact settings</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>They can be overridden in contact and group settings.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4737,11 @@ It can happen because of some bug or when the connection is compromised.</target <target>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>This group has over %lld members, delivery receipts are not sent.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>This group no longer exists.</target> @@ -4613,11 +4762,6 @@ It can happen because of some bug or when the connection is compromised.</target <target>To connect, your contact can scan QR code or use the link in the app.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>To make a new connection</target> @@ -4660,6 +4804,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Toggle incognito when connecting.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Transport isolation</target> @@ -4698,7 +4847,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Unexpected error: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4837,6 +4986,11 @@ To connect, please ask your contact to create another connection link and check <target>Use chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Use current profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Use for new connections</target> @@ -4847,6 +5001,11 @@ To connect, please ask your contact to create another connection link and check <target>Use iOS call interface</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Use new incognito profile</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Use server</target> @@ -5137,9 +5296,9 @@ To connect, please ask your contact to create another connection link and check <target>You have to enter passphrase every time the app starts - it is not stored on the device.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> - <target>You invited your contact</target> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>You invited a contact</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group" xml:space="preserve"> @@ -5267,11 +5426,6 @@ To connect, please ask your contact to create another connection link and check <target>Your chat profile will be sent to group members</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Your chat profile will be sent to your contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Your chat profiles</target> @@ -5326,6 +5480,11 @@ You can change it in Settings.</target> <target>Your privacy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Your profile **%@** will be shared.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5333,11 +5492,6 @@ SimpleX servers cannot see your profile.</source> SimpleX servers cannot see your profile.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Your profile will be sent to the contact that you received this link from</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Your profile, contacts and delivered messages are stored on your device.</target> @@ -5503,6 +5657,11 @@ SimpleX servers cannot see your profile.</target> <target>connected</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <target>connected directly</target> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>connecting</target> @@ -5613,6 +5772,11 @@ SimpleX servers cannot see your profile.</target> <target>direct</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>disabled</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>duplicate message</target> @@ -5693,6 +5857,11 @@ SimpleX servers cannot see your profile.</target> <target>error</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>event happened</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>group deleted</target> @@ -5954,6 +6123,11 @@ SimpleX servers cannot see your profile.</target> <target>security code changed</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <target>send direct message</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>starting…</target> @@ -6098,7 +6272,7 @@ SimpleX servers cannot see your profile.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6130,7 +6304,7 @@ SimpleX servers cannot see your profile.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="en" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index 83c10b08f5..7d429820ee 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/en.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "en", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 ea50b18f5d..85f02bba1a 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="es" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 coloreado!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Historial</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## En respuesta a</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#secreto#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ y %@ conectados</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ a las %2$@:</target> @@ -102,6 +122,11 @@ <target>¡ %@ quiere contactar!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ y %lld miembros más conectados</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,10 @@ <target>%lld minutos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld segundo(s)</target> @@ -302,6 +331,12 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +432,9 @@ <target>Contacto nuevo</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Se enviará un perfil aleatorio al contacto del que recibió este enlace</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Se enviará un perfil aleatorio a tu contacto</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Se compartirá un perfil nuevo aleatorio.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -446,7 +476,7 @@ </trans-unit> <trans-unit id="About SimpleX address" xml:space="preserve"> <source>About SimpleX address</source> - <target>Acerca de dirección SimpleX</target> + <target>Acerca de la dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accent color" xml:space="preserve"> @@ -460,9 +490,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Aceptar contacto</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>¿Aceptar solicitud de conexión?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +503,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Aceptar incógnito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -662,12 +692,12 @@ </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> - <target>Siempre usar relay</target> + <target>Usar siempre retransmisor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> - <target>Se creará un perfil de chat vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre.</target> + <target>Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> @@ -680,6 +710,10 @@ <target>Compilación app: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Icono aplicación</target> @@ -815,9 +849,13 @@ <target>Tanto tú como tu contacto podéis enviar mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> - <target>Mediante perfil de Chat (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> + <target>Mediante perfil (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> @@ -1046,9 +1084,19 @@ <target>Conectar</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>¿Conectar mediante enlace de contacto?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Conectar directamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Conectar incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Conectar mediante enlace de contacto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1114,9 @@ <target>Conecta vía enlace / Código QR</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>¿Conectar mediante enlace de un uso?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Conectar mediante enlace de un sólo uso</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1144,6 @@ <target>Error conexión (Autenticación)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Solicitud de conexión</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>¡Solicitud de conexión enviada!</target> @@ -1183,12 +1226,12 @@ </trans-unit> <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> - <target>Crear dirección SimpleX</target> + <target>Crear tu dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <source>Create an address to let people connect with you.</source> - <target>Crear una dirección para que otras personas se puedan conectar contigo.</target> + <target>Crea una dirección para que otras personas puedan conectar contigo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create file" xml:space="preserve"> @@ -1206,6 +1249,10 @@ <target>Crear enlace</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Crea enlace de invitación de un uso</target> @@ -1223,7 +1270,7 @@ </trans-unit> <trans-unit id="Create your profile" xml:space="preserve"> <source>Create your profile</source> - <target>Crear tu perfil</target> + <target>Crea tu perfil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Created on %@" xml:space="preserve"> @@ -1356,7 +1403,7 @@ </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> - <target>Descentralizado</target> + <target>Descentralizada</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Decryption error" xml:space="preserve"> @@ -1406,12 +1453,12 @@ </trans-unit> <trans-unit id="Delete chat profile" xml:space="preserve"> <source>Delete chat profile</source> - <target>Eliminar perfil de chat</target> + <target>Eliminar perfil</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> - <target>¿Eliminar el perfil de chat?</target> + <target>¿Eliminar el perfil?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> @@ -1446,7 +1493,7 @@ </trans-unit> <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> <source>Delete files for all chat profiles</source> - <target>Eliminar los archivos de todos los perfiles</target> + <target>Eliminar archivos de todos los perfiles</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete for everyone" xml:space="preserve"> @@ -1549,6 +1596,11 @@ <target>Eliminado: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Entrega</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>¡Las confirmaciones de entrega están desactivadas!</target> @@ -1606,7 +1658,7 @@ </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> <source>Disable (keep overrides)</source> - <target>Desactivar (mantener anulaciones)</target> + <target>Desactivar (conservando anulaciones)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1654,6 +1706,10 @@ <target>Desconectar</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Nombre mostrado</target> @@ -1676,7 +1732,7 @@ </trans-unit> <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> - <target>No crear dirección</target> + <target>No crear dirección SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't enable" xml:space="preserve"> @@ -1789,6 +1845,14 @@ <target>¿Cifrar base de datos?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Base de datos cifrada</target> @@ -1914,11 +1978,19 @@ <target>Error al crear enlace de grupo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>¡Error al crear perfil!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Error al eliminar base de datos</target> @@ -2039,6 +2111,10 @@ <target>Error al enviar email</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Error al enviar mensaje</target> @@ -2196,7 +2272,7 @@ </trans-unit> <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> <source>Filter unread and favorite chats.</source> - <target>Filtrar chats no leídos y favoritos.</target> + <target>Filtra chats no leídos y favoritos.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> @@ -2206,7 +2282,7 @@ </trans-unit> <trans-unit id="Find chats faster" xml:space="preserve"> <source>Find chats faster</source> - <target>Encontrar chats mas rápido</target> + <target>Encuentra chats mas rápido</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> @@ -2226,7 +2302,7 @@ </trans-unit> <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> <source>Fix encryption after restoring backups.</source> - <target>Reparar el cifrado tras restaurar copias de seguridad.</target> + <target>Repara el cifrado tras restaurar copias de seguridad.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by contact" xml:space="preserve"> @@ -2406,7 +2482,7 @@ </trans-unit> <trans-unit id="Hidden chat profiles" xml:space="preserve"> <source>Hidden chat profiles</source> - <target>Perfiles Chat ocultos</target> + <target>Perfiles ocultos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden profile password" xml:space="preserve"> @@ -2437,7 +2513,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Historial</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2623,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>En respuesta a</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2635,9 @@ <target>Modo incógnito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>El modo incógnito no se admite aquí, tu perfil principal aparecerá en miembros del grupo</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>La función del modo incógnito es proteger la identidad del perfil principal: por cada contacto nuevo se genera un perfil aleatorio.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2712,11 @@ <target>¡Dirección de servidor no válida!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Estado no válido</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>¡Invitación caducada!</target> @@ -2734,7 +2810,7 @@ </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> - <target>Mantén tus conexiones</target> + <target>Conserva tus conexiones</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="KeyChain error" xml:space="preserve"> @@ -2834,7 +2910,7 @@ </trans-unit> <trans-unit id="Make profile private!" xml:space="preserve"> <source>Make profile private!</source> - <target>¡Hacer un perfil privado!</target> + <target>¡Hacer perfil privado!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> @@ -2869,7 +2945,7 @@ </trans-unit> <trans-unit id="Markdown in messages" xml:space="preserve"> <source>Markdown in messages</source> - <target>Sintaxis markdown en los mensajes</target> + <target>Sintaxis Markdown</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> @@ -2900,7 +2976,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Error en la entrega del mensaje</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3063,11 @@ <target>¡Pronto habrá más mejoras!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Probablemente la conexión ha sido eliminada.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Lo más probable es que este contacto haya eliminado la conexión contigo.</target> @@ -2994,7 +3075,7 @@ </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> - <target>Múltiples perfiles de chat</target> + <target>Múltiples perfiles</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mute" xml:space="preserve"> @@ -3047,6 +3128,10 @@ <target>Nuevo archivo de bases de datos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Nuevo nombre mostrado</target> @@ -3092,6 +3177,11 @@ <target>Sin contactos que añadir</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Sin información de entrega</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>¡Sin dispositivo token!</target> @@ -3198,12 +3288,12 @@ </trans-unit> <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> <source>Only group owners can enable files and media.</source> - <target>Sólo los propietarios pueden activar archivos y multimedia.</target> + <target>Sólo los propietarios del grupo pueden activar los archivos y multimedia.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> <source>Only group owners can enable voice messages.</source> - <target>Sólo los propietarios pueden activar los mensajes de voz.</target> + <target>Sólo los propietarios del grupo pueden activar los mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can add message reactions." xml:space="preserve"> @@ -3256,6 +3346,10 @@ <target>Sólo tu contacto puede enviar mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Abrir Configuración</target> @@ -3346,10 +3440,10 @@ <target>Pegar enlace recibido</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Pega el enlace que has recibido en el recuadro para conectar con tu contacto.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3493,32 +3587,32 @@ </trans-unit> <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> - <target>Prohibir las llamadas y videollamadas.</target> + <target>No se permiten llamadas y videollamadas.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> <source>Prohibit irreversible message deletion.</source> - <target>Prohibir la eliminación irreversible de mensajes.</target> + <target>No se permite la eliminación irreversible de mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit message reactions." xml:space="preserve"> <source>Prohibit message reactions.</source> - <target>Prohibir reacciones a mensajes.</target> + <target>No se permiten reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit messages reactions." xml:space="preserve"> <source>Prohibit messages reactions.</source> - <target>Prohibir reacciones a mensajes.</target> + <target>No se permiten reacciones a los mensajes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> - <target>Prohibir mensajes directos a miembros.</target> + <target>No se permiten mensajes directos entre miembros.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> <source>Prohibit sending disappearing messages.</source> - <target>Prohibir envío de mensajes temporales.</target> + <target>No se permiten mensajes temporales.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending files and media." xml:space="preserve"> @@ -3528,7 +3622,7 @@ </trans-unit> <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> <source>Prohibit sending voice messages.</source> - <target>Prohibir el envío de mensajes de voz.</target> + <target>No se permiten mensajes de voz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> @@ -3538,7 +3632,7 @@ </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> - <target>¡Protege tus perfiles de chat con contraseña!</target> + <target>¡Protege tus perfiles con contraseña!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout" xml:space="preserve"> @@ -3553,7 +3647,7 @@ </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> - <target>Notificaciones push</target> + <target>Notificaciones automáticas</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> @@ -3596,6 +3690,11 @@ <target>Más información en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Las confirmaciones están desactivadas</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Recibido a las</target> @@ -3618,7 +3717,7 @@ </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> - <target>La dirección de recepción se cambiará. El cambio se completará cuando el remitente esté en línea.</target> + <target>La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving file will be stopped." xml:space="preserve"> @@ -3633,7 +3732,7 @@ </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> - <target>Los destinatarios ven actualizaciones mientras les escribes.</target> + <target>Los destinatarios ven actualizarse mientras escribes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> @@ -3648,17 +3747,17 @@ </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> <source>Record updated at</source> - <target>Registro actualizado a las</target> + <target>Registro actualiz.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at: %@" xml:space="preserve"> <source>Record updated at: %@</source> - <target>Registro actualizado a las: %@</target> + <target>Registro actualiz: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Reduced battery usage" xml:space="preserve"> <source>Reduced battery usage</source> - <target>Uso de la batería reducido</target> + <target>Reducción del uso de batería</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject" xml:space="preserve"> @@ -3666,8 +3765,8 @@ <target>Rechazar</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Rechazar contacto (NO se notifica al remitente)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3678,12 +3777,12 @@ </trans-unit> <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> <source>Relay server is only used if necessary. Another party can observe your IP address.</source> - <target>El servidor de retransmisión sólo se usará en caso necesario. Un tercero podría ver tu dirección IP.</target> + <target>El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> <source>Relay server protects your IP address, but it can observe the duration of the call.</source> - <target>El servidor de retransmisión protege tu dirección IP, pero puede observar la duración de la llamada.</target> + <target>El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove" xml:space="preserve"> @@ -3703,7 +3802,7 @@ </trans-unit> <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> <source>Remove passphrase from keychain?</source> - <target>¿Eliminar la contraseña del llavero?</target> + <target>¿Eliminar contraseña de Keychain?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate" xml:space="preserve"> @@ -3848,7 +3947,7 @@ </trans-unit> <trans-unit id="Save auto-accept settings" xml:space="preserve"> <source>Save auto-accept settings</source> - <target>Guardar configuración de aceptación automática (auto-accept)</target> + <target>Guardar configuración de auto aceptar</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> @@ -3986,6 +4085,10 @@ <target>Enviar mensaje directo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Enviar mensaje temporal</target> @@ -4056,11 +4159,21 @@ <target>El envío de confirmaciones está desactivado para %lld contactos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>El envío de confirmaciones está desactivado para %lld grupos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>El envío de confirmaciones está activado para %lld contactos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>El envío de confirmaciones está activado para %lld grupos</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Enviando vía</target> @@ -4201,6 +4314,11 @@ <target>Mostrar opciones de desarrollador</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Mostrar último mensaje</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Mostrar vista previa</target> @@ -4271,6 +4389,10 @@ <target>Invitación SimpleX de un uso</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Omitir</target> @@ -4281,6 +4403,11 @@ <target>Mensajes omitidos</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Grupos pequeños (máx. 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat.</target> @@ -4318,7 +4445,7 @@ </trans-unit> <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> <source>Stop chat to enable database actions</source> - <target>Para habilitar las acciones sobre la base de datos, previamente debes detener Chat</target> + <target>Detén SimpleX para habilitar las acciones sobre la base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> @@ -4525,7 +4652,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="The next generation of private messaging" xml:space="preserve"> <source>The next generation of private messaging</source> - <target>La próxima generación de mensajería privada</target> + <target>La nueva generación de mensajería privada</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> @@ -4560,12 +4687,12 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> </trans-unit> <trans-unit id="There should be at least one user profile." xml:space="preserve"> <source>There should be at least one user profile.</source> - <target>Debe haber al menos un perfil de usuario.</target> + <target>Debe haber al menos un perfil.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> <source>There should be at least one visible user profile.</source> - <target>Debe haber al menos un perfil de usuario visible.</target> + <target>Debe haber al menos un perfil visible.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> @@ -4573,9 +4700,9 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>Esta configuración afecta a tu perfil actual **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Se pueden anular en la configuración de contactos</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Se pueden anular en la configuración de contactos.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4720,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>Esta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Este grupo tiene más de %lld miembros, no se enviarán confirmaciones de entrega.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Este grupo ya no existe.</target> @@ -4613,11 +4745,6 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida.</target> <target>Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Para conocer el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Para hacer una conexión nueva</target> @@ -4647,7 +4774,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t </trans-unit> <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> - <target>Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Mis perfiles**.</target> + <target>Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> @@ -4660,6 +4787,10 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <target>Para comprobar el cifrado de extremo a extremo con tu contacto compara (o escanea) el código en tus dispositivos.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Aislamiento de transporte</target> @@ -4698,7 +4829,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Error inesperado: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4717,7 +4848,7 @@ Se te pedirá que completes la autenticación antes de activar esta función.</t </trans-unit> <trans-unit id="Unhide chat profile" xml:space="preserve"> <source>Unhide chat profile</source> - <target>Mostrar perfil de chat</target> + <target>Mostrar perfil oculto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide profile" xml:space="preserve"> @@ -4838,6 +4969,11 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Usar Chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Usar perfil actual</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Usar para conexiones nuevas</target> @@ -4848,6 +4984,11 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>Usar interfaz de llamada de iOS</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Usar nuevo perfil incógnito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Usar servidor</target> @@ -5025,7 +5166,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> - <target>Tienes un perfil de chat con el mismo nombre mostrado. Debes elegir otro nombre.</target> + <target>Ya tienes un perfil con este nombre mostrado. Por favor, elige otro nombre.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> @@ -5055,7 +5196,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="You can create it later" xml:space="preserve"> <source>You can create it later</source> - <target>Puedes crearlo más tarde</target> + <target>Puedes crearla más tarde</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can enable later via Settings" xml:space="preserve"> @@ -5070,7 +5211,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> <source>You can hide or mute a user profile - swipe it to the right.</source> - <target>Puedes ocultar o silenciar un perfil de usuario: deslízalo hacia la derecha.</target> + <target>Puedes ocultar o silenciar un perfil deslizándolo a la derecha.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can now send messages to %@" xml:space="preserve"> @@ -5138,8 +5279,8 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb <target>La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>Has invitado a tu contacto</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5180,7 +5321,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <source>You will be connected when your connection request is accepted, please wait or check later!</source> - <target>Te conectarás cuando se acepte tu solicitud de conexión, por favor espere o compruébalo más tarde.</target> + <target>Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> @@ -5255,7 +5396,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Your chat database" xml:space="preserve"> <source>Your chat database</source> - <target>Base de datos Chat</target> + <target>Base de datos</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> @@ -5265,12 +5406,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb </trans-unit> <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> <source>Your chat profile will be sent to group members</source> - <target>Tu perfil Chat será enviado a los miembros del grupo</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Tu perfil Chat será enviado a tu contacto</target> + <target>Tu perfil será enviado a los miembros del grupo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> @@ -5327,6 +5463,11 @@ Puedes cambiarlo en Configuración.</target> <target>Privacidad</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Tu perfil **%@** será compartido.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5334,14 +5475,9 @@ SimpleX servers cannot see your profile.</source> Los servidores de SimpleX no pueden ver tu perfil.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Tu perfil se enviará al contacto del que has recibido este enlace</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target>Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</target> + <target>Tu perfil, contactos y mensajes se almacenan en tu dispositivo.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> @@ -5376,7 +5512,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> - <target>[Estrella en GitHub] (https://github.com/simplex-chat/simplex-chat)</target> + <target>[Estrella en GitHub](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="_italic_" xml:space="preserve"> @@ -5504,6 +5640,10 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>conectado</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>conectando</target> @@ -5614,6 +5754,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>directa</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>desactivado</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>mensaje duplicado</target> @@ -5661,12 +5806,12 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> <source>encryption re-negotiation allowed</source> - <target>renegociación de cifrado permitida</target> + <target>renegociar el cifrado permitido</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> <source>encryption re-negotiation allowed for %@</source> - <target>renegociación de cifrado permitida para %@</target> + <target>renegociar el cifrado permitido para %@</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required" xml:space="preserve"> @@ -5694,6 +5839,11 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>error</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>evento ocurrido</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>grupo eliminado</target> @@ -5761,7 +5911,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> - <target>ha invitado a</target> + <target>ha sido invitado</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invited %@" xml:space="preserve"> @@ -5955,6 +6105,10 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> <target>código de seguridad cambiado</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>inicializando…</target> @@ -6002,7 +6156,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </trans-unit> <trans-unit id="via relay" xml:space="preserve"> <source>via relay</source> - <target>mediante servidor relay</target> + <target>mediante retransmisor</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> @@ -6099,7 +6253,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6131,7 +6285,7 @@ Los servidores de SimpleX no pueden ver tu perfil.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="es" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/es.xcloc/contents.json b/apps/ios/SimpleX Localizations/es.xcloc/contents.json index bf017e3f72..c7d2c05ffa 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/es.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "es", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..e919fc253a --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "fi" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 7534b1c199..c7e970f6ff 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2,3905 +2,6306 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="fi" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (" xml:space="preserve" approved="no"> + <trans-unit id=" (" xml:space="preserve"> <source> (</source> - <target state="translated"> (</target> + <target> (</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> + <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> - <target state="translated"> (voidaan kopioida)</target> + <target> (voidaan kopioida)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> + <trans-unit id="!1 colored!" xml:space="preserve"> <source>!1 colored!</source> - <target state="translated">!1 värillinen!</target> + <target>!1 värillinen!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve" approved="no"> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Historia</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## vastauksena</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> - <target state="translated">#salaisuus#</target> + <target>#salaisuus#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve" approved="no"> + <trans-unit id="%@" xml:space="preserve"> <source>%@</source> - <target state="translated">% @</target> + <target>% @</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve" approved="no"> + <trans-unit id="%@ %@" xml:space="preserve"> <source>%@ %@</source> - <target state="translated">%@ % @</target> + <target>%@ % @</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> + <trans-unit id="%@ (current)" xml:space="preserve"> + <source>%@ (current)</source> + <target>%@ (nykyinen)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ (current):" xml:space="preserve"> + <source>%@ (current):</source> + <target>% (nykyinen):</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%@ / %@" xml:space="preserve"> <source>%@ / %@</source> - <target state="translated">%@ / % @</target> + <target>%@ / % @</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ ja %@ yhdistetty</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve"> + <source>%1$@ at %2$@:</source> + <target>%1$@ klo %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> - <target state="translated">%@ on yhdistetty!</target> + <target>%@ on yhdistetty!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> + <trans-unit id="%@ is not verified" xml:space="preserve"> <source>%@ is not verified</source> - <target state="translated">%@ ei ole vahvistettu</target> + <target>%@ ei ole vahvistettu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> + <trans-unit id="%@ is verified" xml:space="preserve"> <source>%@ is verified</source> - <target state="translated">%@ on vahvistettu</target> + <target>%@ on vahvistettu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> + <trans-unit id="%@ servers" xml:space="preserve"> + <source>%@ servers</source> + <target>%@ palvelimet</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> - <target state="translated">%@ haluaa muodostaa yhteyden!</target> + <target>%@ haluaa muodostaa yhteyden!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%d days" xml:space="preserve" approved="no"> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ ja %lld muut jäsenet yhdistetty</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve"> + <source>%@:</source> + <target>%@:</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="%d days" xml:space="preserve"> <source>%d days</source> - <target state="translated">%d päivää</target> - <note>message ttl</note> + <target>%d päivää</target> + <note>time interval</note> </trans-unit> - <trans-unit id="%d hours" xml:space="preserve" approved="no"> + <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> - <target state="translated">%d tuntia</target> - <note>message ttl</note> + <target>%d tuntia</target> + <note>time interval</note> </trans-unit> - <trans-unit id="%d min" xml:space="preserve" approved="no"> + <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> - <target state="translated">%d min</target> - <note>message ttl</note> + <target>%d min</target> + <note>time interval</note> </trans-unit> - <trans-unit id="%d months" xml:space="preserve" approved="no"> + <trans-unit id="%d months" xml:space="preserve"> <source>%d months</source> - <target state="translated">%d kuukautta</target> - <note>message ttl</note> + <target>%d kuukautta</target> + <note>time interval</note> </trans-unit> - <trans-unit id="%d sec" xml:space="preserve" approved="no"> + <trans-unit id="%d sec" xml:space="preserve"> <source>%d sec</source> - <target state="translated">%d sek</target> - <note>message ttl</note> + <target>%d sek</target> + <note>time interval</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> + <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> - <target state="translated">%d ohitettua viestiä</target> + <target>%d ohitettua viestiä</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="%lld" xml:space="preserve" approved="no"> + <trans-unit id="%d weeks" xml:space="preserve"> + <source>%d weeks</source> + <target>%d viikkoa</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%lld" xml:space="preserve"> <source>%lld</source> - <target state="translated">%lld</target> + <target>%lld</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve" approved="no"> + <trans-unit id="%lld %@" xml:space="preserve"> <source>%lld %@</source> - <target state="translated">%lld %@</target> + <target>%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve"> <source>%lld contact(s) selected</source> - <target state="translated">%lld kontaktia valittu</target> + <target>%lld kontaktia valittu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> <source>%lld file(s) with total size of %@</source> - <target state="translated">%lld tiedosto(a), joiden kokonaiskoko on %@</target> + <target>%lld tiedosto(a), joiden kokonaiskoko on %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld members" xml:space="preserve" approved="no"> + <trans-unit id="%lld members" xml:space="preserve"> <source>%lld members</source> - <target state="translated">%lld jäsenet</target> + <target>%lld jäsenet</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> + <trans-unit id="%lld minutes" xml:space="preserve"> + <source>%lld minutes</source> + <target>%lld minuuttia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> - <target state="translated">%lld sekunti(a)</target> + <target>%lld sekunti(a)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldd" xml:space="preserve" approved="no"> + <trans-unit id="%lld seconds" xml:space="preserve"> + <source>%lld seconds</source> + <target>%lld sekuntia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldd" xml:space="preserve"> <source>%lldd</source> - <target state="translated">%lldd</target> + <target>%lldd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldh" xml:space="preserve" approved="no"> + <trans-unit id="%lldh" xml:space="preserve"> <source>%lldh</source> - <target state="translated">%lldh</target> + <target>%lldh</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldk" xml:space="preserve" approved="no"> + <trans-unit id="%lldk" xml:space="preserve"> <source>%lldk</source> - <target state="translated">%lldk</target> + <target>%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldm" xml:space="preserve" approved="no"> + <trans-unit id="%lldm" xml:space="preserve"> <source>%lldm</source> - <target state="translated">%lldm</target> + <target>%lldm</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve" approved="no"> + <trans-unit id="%lldmth" xml:space="preserve"> <source>%lldmth</source> - <target state="translated">%lldmth</target> + <target>%lldmth</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%llds" xml:space="preserve" approved="no"> + <trans-unit id="%llds" xml:space="preserve"> <source>%llds</source> - <target state="translated">%llds</target> + <target>%llds</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldw" xml:space="preserve" approved="no"> + <trans-unit id="%lldw" xml:space="preserve"> <source>%lldw</source> - <target state="translated">%lldw</target> + <target>%lldw</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve" approved="no"> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> + <source>%u messages failed to decrypt.</source> + <target>%u viestien salauksen purku epäonnistui.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%u messages skipped." xml:space="preserve"> + <source>%u messages skipped.</source> + <target>%u viestit ohitettu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="(" xml:space="preserve"> <source>(</source> - <target state="translated">(</target> + <target>(</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve" approved="no"> + <trans-unit id=")" xml:space="preserve"> <source>)</source> - <target state="translated">)</target> + <target>)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> + <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille.</target> + <target>**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> + <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> - <target state="translated">**Luo linkki / QR-koodi* kontaktille.</target> + <target>**Luo linkki / QR-koodi* kontaktille.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> + <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target state="translated">**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on.</target> + <target>**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> + <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target state="translated">**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta).</target> + <target>**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve" approved="no"> + <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target state="translated">**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**.</target> + <target>**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no"> + <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <target state="translated">**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen.</target> + <target>**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> + <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <target state="translated">**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin.</target> + <target>**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve" approved="no"> + <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target state="translated">**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta.</target> + <target>**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no"> + <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <target state="translated">**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin.</target> + <target>**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no"> + <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> - <target state="translated">**e2e-salattu** äänipuhelu</target> + <target>**e2e-salattu** äänipuhelu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve" approved="no"> + <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> <source>**e2e encrypted** video call</source> - <target state="translated">**e2e-salattu** videopuhelu</target> + <target>**e2e-salattu** videopuhelu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="*bold*" xml:space="preserve" approved="no"> + <trans-unit id="*bold*" xml:space="preserve"> <source>\*bold*</source> - <target state="translated">\*bold*</target> + <target>\*bold*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve" approved="no"> + <trans-unit id=", " xml:space="preserve"> <source>, </source> - <target state="translated">, </target> + <target>, </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target>- vakaampi viestien toimitus. +- hieman paremmat ryhmät. +- ja paljon muuta!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> + <source>- voice messages up to 5 minutes. +- custom time to disappear. +- editing history.</source> + <target>- ääniviestit enintään 5 minuuttia. +- mukautettu katoamisaika. +- historian muokkaaminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="." xml:space="preserve"> <source>.</source> + <target>.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="0s" xml:space="preserve"> + <source>0s</source> + <target>0s</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> - <note>message ttl</note> + <target>1 päivä</target> + <note>time interval</note> </trans-unit> <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> - <note>message ttl</note> + <target>1 tunti</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="1 minute" xml:space="preserve"> + <source>1 minute</source> + <target>1 minuutti</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> - <note>message ttl</note> + <target>1 kuukausi</target> + <note>time interval</note> </trans-unit> <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> - <note>message ttl</note> + <target>1 viikko</target> + <note>time interval</note> </trans-unit> - <trans-unit id="2 weeks" xml:space="preserve"> - <source>2 weeks</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="6" xml:space="preserve" approved="no"> - <source>6</source> - <target state="translated">6</target> + <trans-unit id="1-time link" xml:space="preserve"> + <source>1-time link</source> + <target>Kertakäyttölinkki</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve" approved="no"> + <trans-unit id="5 minutes" xml:space="preserve"> + <source>5 minutes</source> + <target>5 minuuttia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="6" xml:space="preserve"> + <source>6</source> + <target>6</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="30 seconds" xml:space="preserve"> + <source>30 seconds</source> + <target>30 sekuntia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id=": " xml:space="preserve"> <source>: </source> - <target state="translated">: </target> + <target>: </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> + <source><p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p></source> + <target><p> Hei! </p> +<p> <a href="%@"> Ollaan yhteydessä SimpleX Chatin kautta</a></p></target> + <note>email text</note> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve"> + <source>A few more things</source> + <target>Muutama asia lisää</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A new contact" xml:space="preserve"> <source>A new contact</source> + <target>Uusi kontakti</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Uusi satunnainen profiili jaetaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> + <target>Erillistä TCP-yhteyttä käytetään **jokaiselle sovelluksessa olevalle chat-profiilille**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> + <target>Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä**. +**Huomaa**: jos kontakteja on useita, akun ja liikenteen kulutus voi olla huomattavasti suurempi ja jotkin yhteydet voivat epäonnistua.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort" xml:space="preserve"> + <source>Abort</source> + <target>Keskeytä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve"> + <source>Abort changing address</source> + <target>Keskeytä osoitteenvaihto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve"> + <source>Abort changing address?</source> + <target>Keskeytä osoitteenvaihto?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX" xml:space="preserve"> <source>About SimpleX</source> + <target>Tietoja SimpleX:stä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> + <target>Tietoja SimpleX Chatistä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About SimpleX address" xml:space="preserve"> + <source>About SimpleX address</source> + <target>Tietoja SimpleX osoitteesta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accent color" xml:space="preserve"> <source>Accent color</source> + <target>Korostusväri</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> + <target>Hyväksy</target> <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Hyväksy yhteyspyyntö?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> <source>Accept contact request from %@?</source> + <target>Hyväksy kontaktipyyntö %@:ltä?</target> <note>notification body</note> </trans-unit> <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> - <note>No comment provided by engineer.</note> + <target>Hyväksy tuntematon</target> + <note>accept contact request via notification</note> </trans-unit> - <trans-unit id="Accept requests" xml:space="preserve"> - <source>Accept requests</source> + <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> + <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target>Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add preset servers" xml:space="preserve"> <source>Add preset servers</source> + <target>Lisää esiasetettuja palvelimia</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add profile" xml:space="preserve"> <source>Add profile</source> + <target>Lisää profiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> + <target>Lisää palvelimia skannaamalla QR-koodeja.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add server…" xml:space="preserve"> <source>Add server…</source> + <target>Lisää palvelin…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add to another device" xml:space="preserve"> <source>Add to another device</source> + <target>Lisää toiseen laitteeseen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> + <target>Lisää tervetuloviesti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address" xml:space="preserve"> + <source>Address</source> + <target>Osoite</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target>Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> + <target>Ylläpitäjät voivat luoda linkkejä ryhmiin liittymiseen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Advanced network settings" xml:space="preserve"> <source>Advanced network settings</source> + <target>Verkon lisäasetukset</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All app data is deleted." xml:space="preserve"> + <source>All app data is deleted.</source> + <target>Kaikki sovelluksen tiedot poistetaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>All chats and messages will be deleted - this cannot be undone!</source> + <target>Kaikki keskustelut ja viestit poistetaan - tätä ei voi kumota!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All data is erased when it is entered." xml:space="preserve"> + <source>All data is erased when it is entered.</source> + <target>Kaikki tiedot poistetaan, kun se syötetään.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> + <target>Kaikki ryhmän jäsenet pysyvät yhteydessä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target>Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All your contacts will remain connected" xml:space="preserve"> - <source>All your contacts will remain connected</source> + <trans-unit id="All your contacts will remain connected." xml:space="preserve"> + <source>All your contacts will remain connected.</source> + <target>Kaikki kontaktisi pysyvät yhteydessä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> + <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target>Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään kontakteillesi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> + <target>Salli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> + <source>Allow calls only if your contact allows them.</source> + <target>Salli puhelut vain, jos kontaktisi sallii ne.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> + <target>Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> + <target>Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> + <source>Allow message reactions only if your contact allows them.</source> + <target>Salli reaktiot viesteihin vain, jos kontaktisi sallii ne.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow message reactions." xml:space="preserve"> + <source>Allow message reactions.</source> + <target>Salli viestireaktiot.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> <source>Allow sending direct messages to members.</source> + <target>Salli yksityisviestien lähettäminen jäsenille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> <source>Allow sending disappearing messages.</source> + <target>Salli katoavien viestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> <source>Allow to irreversibly delete sent messages.</source> + <target>Salli lähetettyjen viestien peruuttamaton poistaminen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send files and media." xml:space="preserve"> + <source>Allow to send files and media.</source> + <target>Salli tiedostojen ja median lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send voice messages." xml:space="preserve"> <source>Allow to send voice messages.</source> + <target>Salli ääniviestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> <source>Allow voice messages only if your contact allows them.</source> + <target>Salli ääniviestit vain, jos kontaktisi sallii ne.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages?" xml:space="preserve"> <source>Allow voice messages?</source> + <target>Salli ääniviestit?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> + <source>Allow your contacts adding message reactions.</source> + <target>Salli kontaktiesi lisätä viestireaktioita.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow your contacts to call you." xml:space="preserve"> + <source>Allow your contacts to call you.</source> + <target>Salli kontaktiesi soittaa sinulle.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> <source>Allow your contacts to irreversibly delete sent messages.</source> + <target>Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> <source>Allow your contacts to send disappearing messages.</source> + <target>Salli kontaktiesi lähettää katoavia viestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> <source>Allow your contacts to send voice messages.</source> + <target>Salli kontaktiesi lähettää ääniviestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Already connected?" xml:space="preserve"> <source>Already connected?</source> + <target>Oletko jo muodostanut yhteyden?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> + <target>Käytä aina relettä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> + <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target>Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> + <target>Vastaa puheluun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> + <target>Sovellusversio: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> + <target>Sovelluksen kuvake</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode" xml:space="preserve"> + <source>App passcode</source> + <target>Sovelluksen pääsykoodi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> + <source>App passcode is replaced with self-destruct passcode.</source> + <target>Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App version" xml:space="preserve"> <source>App version</source> + <target>Sovellusversio</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="App version: v%@" xml:space="preserve"> <source>App version: v%@</source> + <target>Sovellusversio: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Appearance" xml:space="preserve"> <source>Appearance</source> + <target>Ulkonäkö</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> + <target>Liitä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio & video calls" xml:space="preserve"> <source>Audio & video calls</source> + <target>Ääni- ja videopuhelut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Audio and video calls" xml:space="preserve"> <source>Audio and video calls</source> + <target>Ääni- ja videopuhelut</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Audio/video calls" xml:space="preserve"> + <source>Audio/video calls</source> + <target>Ääni/videopuhelut</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> + <source>Audio/video calls are prohibited.</source> + <target>Ääni-/videopuhelut ovat kiellettyjä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication cancelled" xml:space="preserve"> + <source>Authentication cancelled</source> + <target>Tunnistautuminen peruutettu</target> + <note>PIN entry</note> + </trans-unit> <trans-unit id="Authentication failed" xml:space="preserve"> <source>Authentication failed</source> + <target>Tunnistautuminen epäonnistui</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target>Tunnistautuminen vaaditaan ennen kuin puhelu yhdistetään, mutta puheluita voi jäädä vastaamatta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Authentication unavailable" xml:space="preserve"> <source>Authentication unavailable</source> + <target>Tunnistautuminen ei ole käytettävissä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept" xml:space="preserve"> + <source>Auto-accept</source> + <target>Hyväksy automaattisesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept contact requests" xml:space="preserve"> <source>Auto-accept contact requests</source> + <target>Hyväksy yhteydenottopyynnöt automaattisesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept images" xml:space="preserve"> <source>Auto-accept images</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Automatically" xml:space="preserve"> - <source>Automatically</source> + <target>Hyväksy kuvat automaattisesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Back" xml:space="preserve"> <source>Back</source> + <target>Takaisin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad message ID" xml:space="preserve"> + <source>Bad message ID</source> + <target>Virheellinen viestin tunniste</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad message hash" xml:space="preserve"> + <source>Bad message hash</source> + <target>Virheellinen viestin tarkiste</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better messages" xml:space="preserve"> + <source>Better messages</source> + <target>Parempia viestejä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> + <source>Both you and your contact can add message reactions.</source> + <target>Sekä sinä että kontaktisi voivat käyttää viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> <source>Both you and your contact can irreversibly delete sent messages.</source> + <target>Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> + <source>Both you and your contact can make calls.</source> + <target>Sekä sinä että kontaktisi voitte soittaa puheluita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> <source>Both you and your contact can send disappearing messages.</source> + <target>Sekä sinä että kontaktisi voitte lähettää katoavia viestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> <source>Both you and your contact can send voice messages.</source> + <target>Sekä sinä että kontaktisi voitte lähettää ääniviestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target>Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> + <target>Puhelu on jo päättynyt!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Calls" xml:space="preserve"> <source>Calls</source> + <target>Puhelut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't delete user profile!" xml:space="preserve"> <source>Can't delete user profile!</source> + <target>Käyttäjäprofiilia ei voi poistaa!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contact!" xml:space="preserve"> <source>Can't invite contact!</source> + <target>Kontaktia ei voi kutsua!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Can't invite contacts!" xml:space="preserve"> <source>Can't invite contacts!</source> + <target>Kontakteja ei voi kutsua!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> + <target>Peruuta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> <source>Cannot access keychain to save database password</source> + <target>Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> + <target>Tiedostoa ei voi vastaanottaa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change" xml:space="preserve"> <source>Change</source> + <target>Muuta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> + <target>Muutetaanko tietokannan tunnuslause?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change lock mode" xml:space="preserve"> + <source>Change lock mode</source> + <target>Vaihda lukitustilaa</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change member role?" xml:space="preserve"> <source>Change member role?</source> + <target>Vaihda jäsenroolia?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change passcode" xml:space="preserve"> + <source>Change passcode</source> + <target>Vaihda pääsykoodi</target> + <note>authentication reason</note> + </trans-unit> <trans-unit id="Change receiving address" xml:space="preserve"> <source>Change receiving address</source> + <target>Vaihda vastaanotto-osoitetta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change receiving address?" xml:space="preserve"> <source>Change receiving address?</source> + <target>Vaihda vastaanotto-osoite?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Change role" xml:space="preserve"> <source>Change role</source> + <target>Vaihda rooli</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Change self-destruct mode" xml:space="preserve"> + <source>Change self-destruct mode</source> + <target>Vaihda itsetuhotilaa</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change self-destruct passcode" xml:space="preserve"> + <source>Change self-destruct passcode</source> + <target>Vaihda itsetuhoutuva pääsykoodi</target> + <note>authentication reason + set passcode view</note> + </trans-unit> <trans-unit id="Chat archive" xml:space="preserve"> <source>Chat archive</source> + <target>Chat-arkisto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat console" xml:space="preserve"> <source>Chat console</source> + <target>Chat-konsoli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database" xml:space="preserve"> <source>Chat database</source> + <target>Chat-tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database deleted" xml:space="preserve"> <source>Chat database deleted</source> + <target>Chat-tietokanta poistettu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> + <target>Chat-tietokanta tuotu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is running" xml:space="preserve"> <source>Chat is running</source> + <target>Chat on käynnissä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat is stopped" xml:space="preserve"> <source>Chat is stopped</source> + <target>Chat on pysäytetty</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> + <target>Chat-asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> + <target>Keskustelut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> + <target>Tarkista palvelimen osoite ja yritä uudelleen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> + <target>Kiinalainen ja espanjalainen käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> + <target>Valitse tiedosto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Choose from library" xml:space="preserve"> <source>Choose from library</source> + <target>Valitse kirjastosta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> + <target>Tyhjennä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> + <target>Tyhjennä keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear conversation?" xml:space="preserve"> <source>Clear conversation?</source> + <target>Tyhjennä keskustelu?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> + <target>Tyhjennä vahvistus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Colors" xml:space="preserve"> <source>Colors</source> + <target>Värit</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Compare file" xml:space="preserve"> + <source>Compare file</source> + <target>Vertaa tiedostoa</target> + <note>server test step</note> + </trans-unit> <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> <source>Compare security codes with your contacts.</source> + <target>Vertaa turvakoodeja kontaktiesi kanssa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> + <target>Määritä ICE-palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> + <target>Vahvista</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm Passcode" xml:space="preserve"> + <source>Confirm Passcode</source> + <target>Vahvista pääsykoodi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm database upgrades" xml:space="preserve"> + <source>Confirm database upgrades</source> + <target>Vahvista tietokannan päivitykset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> + <target>Vahvista uusi tunnuslause…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Confirm password" xml:space="preserve"> <source>Confirm password</source> + <target>Vahvista salasana</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> + <target>Yhdistä</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Yhdistä suoraan</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Yhdistä Incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Yhdistä kontaktilinkillä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> <source>Connect via group link?</source> + <target>Yhdistetäänkö ryhmälinkin kautta?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link" xml:space="preserve"> <source>Connect via link</source> + <target>Yhdistä linkin kautta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via link / QR code" xml:space="preserve"> <source>Connect via link / QR code</source> + <target>Yhdistä linkillä / QR-koodilla</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Yhdistä kertalinkillä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> + <target>Yhteyden muodostaminen palvelimeen…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> <source>Connecting to server… (error: %@)</source> + <target>Yhteyden muodostaminen palvelimeen... (virhe: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection" xml:space="preserve"> <source>Connection</source> + <target>Yhteys</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> + <target>Yhteysvirhe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection error (AUTH)" xml:space="preserve"> <source>Connection error (AUTH)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> + <target>Yhteysvirhe (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> + <target>Yhteyspyyntö lähetetty!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connection timeout" xml:space="preserve"> <source>Connection timeout</source> + <target>Yhteyden aikakatkaisu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> + <target>Kontakti sallii</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact already exists" xml:space="preserve"> <source>Contact already exists</source> + <target>Kontakti on jo olemassa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>Contact and all messages will be deleted - this cannot be undone!</source> + <target>Kontakti ja kaikki viestit poistetaan - tätä ei voi perua!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> + <target>Kontakti piilotettu:</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is connected" xml:space="preserve"> <source>Contact is connected</source> + <target>Kontakti on yhdistetty</target> <note>notification</note> </trans-unit> <trans-unit id="Contact is not connected yet!" xml:space="preserve"> <source>Contact is not connected yet!</source> + <target>Kontaktia ei ole vielä yhdistetty!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact name" xml:space="preserve"> <source>Contact name</source> + <target>Kontaktin nimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contact preferences" xml:space="preserve"> <source>Contact preferences</source> + <target>Kontaktin asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact requests" xml:space="preserve"> - <source>Contact requests</source> + <trans-unit id="Contacts" xml:space="preserve"> + <source>Contacts</source> + <target>Kontaktit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target>Kontaktit voivat merkitä viestit poistettaviksi; voit katsella niitä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Continue" xml:space="preserve"> + <source>Continue</source> + <target>Jatka</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> + <target>Kopioi</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Core built at: %@" xml:space="preserve"> - <source>Core built at: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> + <target>Ydinversio: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create" xml:space="preserve"> <source>Create</source> + <target>Luo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create address" xml:space="preserve"> - <source>Create address</source> + <trans-unit id="Create SimpleX address" xml:space="preserve"> + <source>Create SimpleX address</source> + <target>Luo SimpleX-osoite</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> + <source>Create an address to let people connect with you.</source> + <target>Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create file" xml:space="preserve"> + <source>Create file</source> + <target>Luo tiedosto</target> + <note>server test step</note> + </trans-unit> <trans-unit id="Create group link" xml:space="preserve"> <source>Create group link</source> + <target>Luo ryhmälinkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create link" xml:space="preserve"> <source>Create link</source> + <target>Luo linkki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> + <target>Luo kertakutsulinkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create queue" xml:space="preserve"> <source>Create queue</source> + <target>Luo jono</target> <note>server test step</note> </trans-unit> <trans-unit id="Create secret group" xml:space="preserve"> <source>Create secret group</source> + <target>Luo salainen ryhmä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Create your profile" xml:space="preserve"> <source>Create your profile</source> + <target>Luo profiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Created on %@" xml:space="preserve"> <source>Created on %@</source> + <target>Luotu %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current Passcode" xml:space="preserve"> + <source>Current Passcode</source> + <target>Nykyinen pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> + <target>Nykyinen tunnuslause…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> + <target>Nykyinen tuettu enimmäistiedostokoko on %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Custom time" xml:space="preserve"> + <source>Custom time</source> + <target>Mukautettu aika</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> + <target>Tumma</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> + <target>Tietokannan tunnus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database ID: %d" xml:space="preserve"> + <source>Database ID: %d</source> + <target>Tietokannan tunnus: %d</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> + <source>Database IDs and Transport isolation option.</source> + <target>Tietokantatunnukset ja kuljetuseristysvaihtoehto.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade" xml:space="preserve"> + <source>Database downgrade</source> + <target>Tietokannan alentaminen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encrypted!" xml:space="preserve"> <source>Database encrypted!</source> + <target>Tietokanta salattu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> <source>Database encryption passphrase will be updated and stored in the keychain. </source> + <target>Tietokannan salaustunnuslause päivitetään ja tallennetaan avainnippuun. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> <source>Database encryption passphrase will be updated. </source> + <target>Tietokannan salauksen tunnuslause päivitetään. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database error" xml:space="preserve"> <source>Database error</source> + <target>Tietokantavirhe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> <source>Database is encrypted using a random passphrase, you can change it.</source> + <target>Tietokanta on salattu satunnaisella tunnuslauseella, voit muuttaa sitä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target>Tietokanta on salattu satunnaisella tunnuslauseella. Vaihda se ennen vientiä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase" xml:space="preserve"> <source>Database passphrase</source> + <target>Tietokannan tunnuslause</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase & export" xml:space="preserve"> <source>Database passphrase & export</source> + <target>Tietokannan tunnuslause ja vienti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> <source>Database passphrase is different from saved in the keychain.</source> + <target>Tietokannan tunnuslause eroaa avainnippuun tallennetusta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> <source>Database passphrase is required to open chat.</source> + <target>Keskustelun avaamiseen tarvitaan tietokannan tunnuslause.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade" xml:space="preserve"> + <source>Database upgrade</source> + <target>Tietokannan päivitys</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> <source>Database will be encrypted and the passphrase stored in the keychain. </source> + <target>Tietokanta salataan ja tunnuslause tallennetaan avainnippuun. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be encrypted. " xml:space="preserve"> <source>Database will be encrypted. </source> + <target>Tietokanta salataan. +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> <source>Database will be migrated when the app restarts</source> + <target>Tietokanta siirretään, kun sovellus käynnistyy uudelleen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> + <target>Hajautettu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Decryption error" xml:space="preserve"> + <source>Decryption error</source> + <target>Salauksen purkuvirhe</target> + <note>message decrypt error item</note> + </trans-unit> <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> + <target>Poista</target> <note>chat item action</note> </trans-unit> <trans-unit id="Delete Contact" xml:space="preserve"> <source>Delete Contact</source> + <target>Poista kontakti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> + <target>Poista osoite</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete address?" xml:space="preserve"> <source>Delete address?</source> + <target>Poista osoite?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete after" xml:space="preserve"> <source>Delete after</source> + <target>Poista jälkeen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete all files" xml:space="preserve"> <source>Delete all files</source> + <target>Poista kaikki tiedostot</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete archive" xml:space="preserve"> <source>Delete archive</source> + <target>Poista arkisto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat archive?" xml:space="preserve"> <source>Delete chat archive?</source> + <target>Poista keskusteluarkisto?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat profile" xml:space="preserve"> + <source>Delete chat profile</source> + <target>Poista keskusteluprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> + <target>Poista keskusteluprofiili?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> + <target>Poista yhteys</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete contact" xml:space="preserve"> <source>Delete contact</source> + <target>Poista kontakti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete contact?" xml:space="preserve"> <source>Delete contact?</source> + <target>Poista kontakti?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete database" xml:space="preserve"> <source>Delete database</source> + <target>Poista tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Delete file" xml:space="preserve"> + <source>Delete file</source> + <target>Poista tiedosto</target> + <note>server test step</note> + </trans-unit> <trans-unit id="Delete files and media?" xml:space="preserve"> <source>Delete files and media?</source> + <target>Poista tiedostot ja media?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> <source>Delete files for all chat profiles</source> + <target>Poista tiedostot kaikista keskusteluprofiileista</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete for everyone" xml:space="preserve"> <source>Delete for everyone</source> + <target>Poista kaikilta</target> <note>chat feature</note> </trans-unit> <trans-unit id="Delete for me" xml:space="preserve"> <source>Delete for me</source> + <target>Poista minulta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete group" xml:space="preserve"> <source>Delete group</source> + <target>Poista ryhmä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete group?" xml:space="preserve"> <source>Delete group?</source> + <target>Poista ryhmä?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete invitation" xml:space="preserve"> <source>Delete invitation</source> + <target>Poista kutsu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete link" xml:space="preserve"> <source>Delete link</source> + <target>Poista linkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete link?" xml:space="preserve"> <source>Delete link?</source> + <target>Poista linkki?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> + <target>Poista jäsenviesti?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete message?" xml:space="preserve"> <source>Delete message?</source> + <target>Poista viesti?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> + <target>Poista viestit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> + <target>Poista viestit tämän jälkeen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database" xml:space="preserve"> <source>Delete old database</source> + <target>Poista vanha tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete old database?" xml:space="preserve"> <source>Delete old database?</source> + <target>Poista vanha tietokanta?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection" xml:space="preserve"> <source>Delete pending connection</source> + <target>Poista vireillä oleva yhteys</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete pending connection?" xml:space="preserve"> <source>Delete pending connection?</source> + <target>Poistetaanko odottava yhteys?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete profile" xml:space="preserve"> + <source>Delete profile</source> + <target>Poista profiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delete queue" xml:space="preserve"> <source>Delete queue</source> + <target>Poista jono</target> <note>server test step</note> </trans-unit> <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> + <target>Poista käyttäjäprofiili?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted at" xml:space="preserve"> + <source>Deleted at</source> + <target>Poistettu klo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted at: %@" xml:space="preserve"> + <source>Deleted at: %@</source> + <target>Poistettu klo: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Toimitus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> + <source>Delivery receipts are disabled!</source> + <target>Toimituskuittaukset poissa käytöstä!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts!" xml:space="preserve"> + <source>Delivery receipts!</source> + <target>Toimituskuittaukset!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Description" xml:space="preserve"> <source>Description</source> + <target>Kuvaus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> + <target>Kehitä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> + <target>Kehittäjätyökalut</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device" xml:space="preserve"> <source>Device</source> + <target>Laite</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> + <target>Laitteen todennus on poistettu käytöstä. SimpleX Lock kytketään pois päältä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> + <target>Laitteen todennus ei ole käytössä. Voit ottaa SimpleX Lockin käyttöön Asetuksista, kun olet ottanut laitteen todennuksen käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> <source>Different names, avatars and transport isolation.</source> + <target>Eri nimet, avatarit ja kuljetuseristys.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Direct messages" xml:space="preserve"> <source>Direct messages</source> + <target>Yksityisviestit</target> <note>chat feature</note> </trans-unit> <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> <source>Direct messages between members are prohibited in this group.</source> + <target>Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve"> + <source>Disable (keep overrides)</source> + <target>Poista käytöstä (pidä ohitukset)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> <source>Disable SimpleX Lock</source> + <target>Poista SimpleX Lock käytöstä</target> <note>authentication reason</note> </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve"> + <source>Disable for all</source> + <target>Poista käytöstä kaikilta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing message" xml:space="preserve"> + <source>Disappearing message</source> + <target>Tuhoutuva viesti</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Disappearing messages" xml:space="preserve"> <source>Disappearing messages</source> + <target>Tuhoutuvat viestit</target> <note>chat feature</note> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> <source>Disappearing messages are prohibited in this chat.</source> + <target>Katoavat viestit ovat kiellettyjä tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> <source>Disappearing messages are prohibited in this group.</source> + <target>Katoavat viestit ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Disappears at" xml:space="preserve"> + <source>Disappears at</source> + <target>Katoaa klo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappears at: %@" xml:space="preserve"> + <source>Disappears at: %@</source> + <target>Katoaa klo: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> <source>Disconnect</source> + <target>Katkaise</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> + <target>Näyttönimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Display name:" xml:space="preserve"> <source>Display name:</source> + <target>Näyttönimi:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> + <target>Älä käytä SimpleX-sovellusta hätäpuheluihin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> + <target>Tee myöhemmin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't create address" xml:space="preserve"> + <source>Don't create address</source> + <target>Älä luo osoitetta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve"> + <source>Don't enable</source> + <target>Älä salli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> + <target>Älä näytä uudelleen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Downgrade and open chat" xml:space="preserve"> + <source>Downgrade and open chat</source> + <target>Alenna ja avaa keskustelu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download file" xml:space="preserve"> + <source>Download file</source> + <target>Lataa tiedosto</target> + <note>server test step</note> + </trans-unit> <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> + <target>Päällekkäinen näyttönimi!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Duration" xml:space="preserve"> + <source>Duration</source> + <target>Kesto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> + <target>Muokkaa</target> <note>chat item action</note> </trans-unit> <trans-unit id="Edit group profile" xml:space="preserve"> <source>Edit group profile</source> + <target>Muokkaa ryhmäprofiilia</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable" xml:space="preserve"> <source>Enable</source> + <target>Salli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve"> + <source>Enable (keep overrides)</source> + <target>Salli (pidä ohitukset)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> + <target>Ota SimpleX Lock käyttöön</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> <source>Enable TCP keep-alive</source> + <target>Ota TCP-säilytys käyttöön</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> + <target>Ota automaattinen viestien poisto käyttöön?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve"> + <source>Enable for all</source> + <target>Salli kaikille</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> + <target>Salli välittömät ilmoitukset?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable lock" xml:space="preserve"> + <source>Enable lock</source> + <target>Ota lukitus käyttöön</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable notifications" xml:space="preserve"> <source>Enable notifications</source> + <target>Salli ilmoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable periodic notifications?" xml:space="preserve"> <source>Enable periodic notifications?</source> + <target>Salli säännölliset ilmoitukset?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enable self-destruct" xml:space="preserve"> + <source>Enable self-destruct</source> + <target>Ota itsetuho käyttöön</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> + <source>Enable self-destruct passcode</source> + <target>Ota itsetuhoava pääsykoodi käyttöön</target> + <note>set passcode view</note> + </trans-unit> <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> + <target>Salaa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypt database?" xml:space="preserve"> <source>Encrypt database?</source> + <target>Salaa tietokanta?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Salaa paikalliset tiedostot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> + <target>Salattu tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Encrypted message or another event" xml:space="preserve"> <source>Encrypted message or another event</source> + <target>Salattu viesti tai muu tapahtuma</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> + <target>Salattu viesti: tietokantavirhe</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> + <source>Encrypted message: database migration error</source> + <target>Salattu viesti: tietokannan siirtovirhe</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> <source>Encrypted message: keychain error</source> + <target>Salattu viesti: avainnipun virhe</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> <source>Encrypted message: no passphrase</source> + <target>Salattu viesti: ei tunnuslausetta</target> <note>notification</note> </trans-unit> <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> <source>Encrypted message: unexpected error</source> + <target>Salattu viesti: odottamaton virhe</target> <note>notification</note> </trans-unit> + <trans-unit id="Enter Passcode" xml:space="preserve"> + <source>Enter Passcode</source> + <target>Syötä pääsykoodi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Enter correct passphrase." xml:space="preserve"> <source>Enter correct passphrase.</source> + <target>Anna oikea tunnuslause.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> + <target>Syötä tunnuslause…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter password above to show!" xml:space="preserve"> <source>Enter password above to show!</source> + <target>Kirjoita yllä oleva salasana näyttääksesi!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enter server manually" xml:space="preserve"> <source>Enter server manually</source> + <target>Syötä palvelin manuaalisesti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Enter welcome message…" xml:space="preserve"> + <source>Enter welcome message…</source> + <target>Kirjoita tervetuloviesti…</target> + <note>placeholder</note> + </trans-unit> + <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> + <source>Enter welcome message… (optional)</source> + <target>Kirjoita tervetuloviesti... (valinnainen)</target> + <note>placeholder</note> + </trans-unit> <trans-unit id="Error" xml:space="preserve"> <source>Error</source> + <target>Virhe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error aborting address change" xml:space="preserve"> + <source>Error aborting address change</source> + <target>Virhe osoitteenmuutoksen keskeytyksessä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> + <target>Virhe kontaktipyynnön hyväksymisessä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error accessing database file" xml:space="preserve"> <source>Error accessing database file</source> + <target>Virhe tietokantatiedoston käyttämisessä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> + <target>Virhe lisättäessä jäseniä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> + <target>Virhe osoitteenvaihdossa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> + <target>Virhe roolin vaihdossa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error changing setting" xml:space="preserve"> <source>Error changing setting</source> + <target>Virhe asetuksen muuttamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> + <target>Virhe osoitteen luomisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating group" xml:space="preserve"> <source>Error creating group</source> + <target>Virhe ryhmän luomisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating group link" xml:space="preserve"> <source>Error creating group link</source> + <target>Virhe ryhmälinkin luomisessa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> + <target>Virhe profiilin luomisessa!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Virhe tiedoston salauksen purussa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> + <target>Virhe keskustelujen tietokannan poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting chat!" xml:space="preserve"> <source>Error deleting chat!</source> + <target>Virhe keskutelun poistamisessa!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting connection" xml:space="preserve"> <source>Error deleting connection</source> + <target>Virhe yhteyden poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting contact" xml:space="preserve"> <source>Error deleting contact</source> + <target>Virhe kontaktin poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> + <target>Virhe tietokannan poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting old database" xml:space="preserve"> <source>Error deleting old database</source> + <target>Virhe vanhan tietokannan poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting token" xml:space="preserve"> <source>Error deleting token</source> + <target>Virhe tokenin poistamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error deleting user profile" xml:space="preserve"> <source>Error deleting user profile</source> + <target>Virhe käyttäjäprofiilin poistamisessa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> + <source>Error enabling delivery receipts!</source> + <target>Virhe toimituskuittauksien sallimisessa!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error enabling notifications" xml:space="preserve"> <source>Error enabling notifications</source> + <target>Virhe ilmoitusten käyttöönotossa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error encrypting database" xml:space="preserve"> <source>Error encrypting database</source> + <target>Virhe tietokannan salauksessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error exporting chat database" xml:space="preserve"> <source>Error exporting chat database</source> + <target>Virhe vietäessä keskustelujen tietokantaa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> + <target>Virhe keskustelujen tietokannan tuonnissa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error joining group" xml:space="preserve"> <source>Error joining group</source> + <target>Virhe ryhmään liittymisessä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error loading %@ servers" xml:space="preserve"> + <source>Error loading %@ servers</source> + <target>Virhe %@-palvelimien lataamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> + <target>Virhe tiedoston vastaanottamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> + <target>Virhe poistettaessa jäsentä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving %@ servers" xml:space="preserve"> + <source>Error saving %@ servers</source> + <target>Virhe %@ palvelimien tallentamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving ICE servers" xml:space="preserve"> <source>Error saving ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving SMP servers" xml:space="preserve"> - <source>Error saving SMP servers</source> + <target>Virhe ICE-palvelimien tallentamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> + <target>Virhe ryhmäprofiilin tallentamisessa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving passcode" xml:space="preserve"> + <source>Error saving passcode</source> + <target>Virhe pääsykoodin tallentamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> <source>Error saving passphrase to keychain</source> + <target>Virhe tunnuslauseen tallentamisessa avainnippuun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> + <target>Virhe käyttäjän salasanan tallentamisessa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending email" xml:space="preserve"> + <source>Error sending email</source> + <target>Virhe sähköpostin lähettämisessä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> + <target>Virhe viestin lähettämisessä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> + <source>Error setting delivery receipts!</source> + <target>Virhe toimituskuittauksien asettamisessa!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> <source>Error starting chat</source> + <target>Virhe käynnistettäessä keskustelua</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error stopping chat" xml:space="preserve"> <source>Error stopping chat</source> + <target>Virhe keskustelun lopettamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> + <target>Virhe profiilin vaihdossa!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error synchronizing connection" xml:space="preserve"> + <source>Error synchronizing connection</source> + <target>Virhe yhteyden synkronoinnissa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> + <target>Virhe ryhmälinkin päivittämisessä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating message" xml:space="preserve"> <source>Error updating message</source> + <target>Virhe viestin päivityksessä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> + <target>Virhe asetusten päivittämisessä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating user privacy" xml:space="preserve"> <source>Error updating user privacy</source> + <target>Virhe päivitettäessä käyttäjän tietosuojaa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: " xml:space="preserve"> + <source>Error: </source> + <target>Virhe: </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> + <target>Virhe: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> + <target>Virhe: URL on virheellinen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error: no database file" xml:space="preserve"> <source>Error: no database file</source> + <target>Virhe: ei tietokantatiedostoa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> + <source>Even when disabled in the conversation.</source> + <target>Jopa kun ei käytössä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exit without saving" xml:space="preserve"> <source>Exit without saving</source> + <target>Poistu tallentamatta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> + <target>Vie tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Export error:" xml:space="preserve"> <source>Export error:</source> + <target>Vientivirhe:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> + <target>Viety tietokanta-arkisto.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve"> - <source>Exporting database archive...</source> + <trans-unit id="Exporting database archive…" xml:space="preserve"> + <source>Exporting database archive…</source> + <target>Tietokanta-arkiston vienti…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Failed to remove passphrase" xml:space="preserve"> <source>Failed to remove passphrase</source> + <target>Tunnuslauseen poisto epäonnistui</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> + <source>Fast and no wait until the sender is online!</source> + <target>Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Favorite" xml:space="preserve"> + <source>Favorite</source> + <target>Suosikki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be deleted from servers." xml:space="preserve"> + <source>File will be deleted from servers.</source> + <target>Tiedosto poistetaan palvelimilta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> + <source>File will be received when your contact completes uploading it.</source> + <target>Tiedosto vastaanotetaan, kun kontaktisi on ladannut sen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> + <target>Tiedosto vastaanotetaan, kun kontakti on online-tilassa, odota tai tarkista myöhemmin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File: %@" xml:space="preserve"> <source>File: %@</source> + <target>Tiedosto: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> + <target>Tiedostot & media</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media" xml:space="preserve"> + <source>Files and media</source> + <target>Tiedostot ja media</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> + <source>Files and media are prohibited in this group.</source> + <target>Tiedostot ja media ovat tässä ryhmässä kiellettyjä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media prohibited!" xml:space="preserve"> + <source>Files and media prohibited!</source> + <target>Tiedostot ja media kielletty!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> + <source>Filter unread and favorite chats.</source> + <target>Suodata lukemattomia- ja suosikkikeskusteluja.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> + <source>Finally, we have them! 🚀</source> + <target>Vihdoinkin meillä! 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Find chats faster" xml:space="preserve"> + <source>Find chats faster</source> + <target>Löydä keskustelut nopeammin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix" xml:space="preserve"> + <source>Fix</source> + <target>Korjaa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection" xml:space="preserve"> + <source>Fix connection</source> + <target>Korjaa yhteys</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection?" xml:space="preserve"> + <source>Fix connection?</source> + <target>Korjaa yhteys?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> + <source>Fix encryption after restoring backups.</source> + <target>Korjaa salaus varmuuskopioiden palauttamisen jälkeen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by contact" xml:space="preserve"> + <source>Fix not supported by contact</source> + <target>Kontakti ei tue korjausta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by group member" xml:space="preserve"> + <source>Fix not supported by group member</source> + <target>Ryhmän jäsen ei tue korjausta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="For console" xml:space="preserve"> <source>For console</source> + <target>Konsoliin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="French interface" xml:space="preserve"> <source>French interface</source> + <target>Ranskalainen käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full link" xml:space="preserve"> <source>Full link</source> + <target>Koko linkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full name (optional)" xml:space="preserve"> <source>Full name (optional)</source> + <target>Koko nimi (valinnainen)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Full name:" xml:space="preserve"> <source>Full name:</source> + <target>Koko nimi:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <source>Fully re-implemented - work in background!</source> + <target>Täysin uudistettu - toimii taustalla!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Further reduced battery usage" xml:space="preserve"> <source>Further reduced battery usage</source> + <target>Entistä pienempi akun käyttö</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="GIFs and stickers" xml:space="preserve"> <source>GIFs and stickers</source> + <target>GIFit ja tarrat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group" xml:space="preserve"> <source>Group</source> + <target>Ryhmä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group display name" xml:space="preserve"> <source>Group display name</source> + <target>Ryhmän näyttönimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group full name (optional)" xml:space="preserve"> <source>Group full name (optional)</source> + <target>Ryhmän näyttönimi (valinnainen)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group image" xml:space="preserve"> <source>Group image</source> + <target>Ryhmäkuva</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation" xml:space="preserve"> <source>Group invitation</source> + <target>Ryhmän kutsu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation expired" xml:space="preserve"> <source>Group invitation expired</source> + <target>Vanhentunut ryhmäkutsu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> <source>Group invitation is no longer valid, it was removed by sender.</source> + <target>Ryhmäkutsu ei ole enää voimassa, lähettäjä poisti sen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group link" xml:space="preserve"> <source>Group link</source> + <target>Ryhmälinkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group links" xml:space="preserve"> <source>Group links</source> + <target>Ryhmälinkit</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can add message reactions." xml:space="preserve"> + <source>Group members can add message reactions.</source> + <target>Ryhmän jäsenet voivat lisätä viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> <source>Group members can irreversibly delete sent messages.</source> + <target>Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send direct messages." xml:space="preserve"> <source>Group members can send direct messages.</source> + <target>Ryhmän jäsenet voivat lähettää suoraviestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> <source>Group members can send disappearing messages.</source> + <target>Ryhmän jäsenet voivat lähettää katoavia viestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can send files and media." xml:space="preserve"> + <source>Group members can send files and media.</source> + <target>Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send voice messages." xml:space="preserve"> <source>Group members can send voice messages.</source> + <target>Ryhmän jäsenet voivat lähettää ääniviestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> + <target>Ryhmäviesti:</target> <note>notification</note> </trans-unit> <trans-unit id="Group moderation" xml:space="preserve"> <source>Group moderation</source> + <target>Ryhmän moderointi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group preferences" xml:space="preserve"> <source>Group preferences</source> + <target>Ryhmän asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile" xml:space="preserve"> <source>Group profile</source> + <target>Ryhmäprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> <source>Group profile is stored on members' devices, not on the servers.</source> + <target>Ryhmäprofiili tallennetaan jäsenten laitteille, ei palvelimille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group welcome message" xml:space="preserve"> <source>Group welcome message</source> + <target>Ryhmän tervetuloviesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> + <target>Ryhmä poistetaan kaikilta jäseniltä - tätä ei voi kumota!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for you - this cannot be undone!</source> + <target>Ryhmä poistetaan sinulta - tätä ei voi perua!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Help" xml:space="preserve"> <source>Help</source> + <target>Apua</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> + <target>Piilotettu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden chat profiles" xml:space="preserve"> <source>Hidden chat profiles</source> + <target>Piilotetut keskusteluprofiilit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hidden profile password" xml:space="preserve"> <source>Hidden profile password</source> + <target>Piilotettu profiilin salasana</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide" xml:space="preserve"> <source>Hide</source> + <target>Piilota</target> <note>chat item action</note> </trans-unit> <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> <source>Hide app screen in the recent apps.</source> + <target>Piilota sovellusnäyttö viimeisimmissä sovelluksissa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Hide profile" xml:space="preserve"> <source>Hide profile</source> + <target>Piilota profiili</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide:" xml:space="preserve"> + <source>Hide:</source> + <target>Piilota:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="History" xml:space="preserve"> + <source>History</source> + <target>Historia</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> + <target>Miten SimpleX toimii</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> + <target>Kuinka se toimii</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to" xml:space="preserve"> <source>How to</source> + <target>Miten</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use it" xml:space="preserve"> <source>How to use it</source> + <target>Kuinka sitä käytetään</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How to use your servers" xml:space="preserve"> <source>How to use your servers</source> + <target>Miten käytät palvelimiasi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> + <target>ICE-palvelimet (yksi per rivi)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you can't meet in person, **show QR code in the video call**, or share the link." xml:space="preserve"> - <source>If you can't meet in person, **show QR code in the video call**, or share the link.</source> + <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> + <source>If you can't meet in person, show QR code in a video call, or share the link.</source> + <target>Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> + <target>Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> + <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> + <target>Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> + <source>If you enter your self-destruct passcode while opening the app:</source> + <target>Jos syötät itsetuhoutuvan pääsykoodin sovellusta avattaessa:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> + <target>Jos haluat käyttää keskustelua nyt, napauta **Tee se myöhemmin** alla (sinulle tarjotaan tietokannan siirtämistä, kun käynnistät sovelluksen uudelleen).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Ignore" xml:space="preserve"> <source>Ignore</source> + <target>Sivuuta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> + <source>Image will be received when your contact completes uploading it.</source> + <target>Kuva vastaanotetaan, kun kontaktisi on ladannut sen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Image will be received when your contact is online, please wait or check later!</source> + <target>Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Immediately" xml:space="preserve"> + <source>Immediately</source> + <target>Heti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Immune to spam and abuse" xml:space="preserve"> <source>Immune to spam and abuse</source> + <target>Immuuni roskapostille ja väärinkäytöksille</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import" xml:space="preserve"> <source>Import</source> + <target>Tuo</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import chat database?" xml:space="preserve"> <source>Import chat database?</source> + <target>Tuo keskustelujen-tietokanta?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Import database" xml:space="preserve"> <source>Import database</source> + <target>Tuo tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> + <target>Parannettu yksityisyys ja turvallisuus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Improved server configuration" xml:space="preserve"> <source>Improved server configuration</source> + <target>Parannettu palvelimen kokoonpano</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In reply to" xml:space="preserve"> + <source>In reply to</source> + <target>Vastauksena</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> + <target>Incognito</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito mode" xml:space="preserve"> <source>Incognito mode</source> + <target>Incognito-tila</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Incognito-tila suojaa yksityisyyttäsi käyttämällä uutta satunnaista profiilia jokaiselle kontaktille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> <source>Incoming audio call</source> + <target>Saapuva äänipuhelu</target> <note>notification</note> </trans-unit> <trans-unit id="Incoming call" xml:space="preserve"> <source>Incoming call</source> + <target>Saapuva puhelu</target> <note>notification</note> </trans-unit> <trans-unit id="Incoming video call" xml:space="preserve"> <source>Incoming video call</source> + <target>Saapuva videopuhelu</target> <note>notification</note> </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Yhteensopimaton tietokantaversio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incorrect passcode" xml:space="preserve"> + <source>Incorrect passcode</source> + <target>Väärä pääsykoodi</target> + <note>PIN entry</note> + </trans-unit> <trans-unit id="Incorrect security code!" xml:space="preserve"> <source>Incorrect security code!</source> + <target>Väärä turvakoodi!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Info" xml:space="preserve"> + <source>Info</source> + <target>Tiedot</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Initial role" xml:space="preserve"> + <source>Initial role</source> + <target>Alkuperäinen rooli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> + <target>Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> + <target>Välittömät push-ilmoitukset ovat piilossa! +</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Instantly" xml:space="preserve"> <source>Instantly</source> + <target>Heti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> + <target>Käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> + <target>Virheellinen yhteyslinkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> + <target>Virheellinen palvelinosoite!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Virheellinen tila</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> + <target>Vanhentunut kutsu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite friends" xml:space="preserve"> + <source>Invite friends</source> + <target>Kutsu ystäviä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite members" xml:space="preserve"> <source>Invite members</source> + <target>Kutsu jäseniä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> + <target>Kutsu ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion" xml:space="preserve"> <source>Irreversible message deletion</source> + <target>Peruuttamaton viestin poisto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this chat.</source> + <target>Viestien peruuttamaton poisto on kielletty tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this group.</source> + <target>Viestien peruuttamaton poisto on kielletty tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> + <target>Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="It can happen when: 1. The messages expire on the server if they were not received for 30 days, 2. The server you use to receive the messages from this contact was updated and restarted. 3. The connection is compromised. Please connect to the developers via Settings to receive the updates about the servers. We will be adding server redundancy to prevent lost messages." xml:space="preserve"> + <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> + <source>It can happen when you or your connection used the old database backup.</source> + <target>Se voi tapahtua, kun sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> <source>It can happen when: -1. The messages expire on the server if they were not received for 30 days, -2. The server you use to receive the messages from this contact was updated and restarted. -3. The connection is compromised. -Please connect to the developers via Settings to receive the updates about the servers. -We will be adding server redundancy to prevent lost messages.</source> +1. The messages expired in the sending client after 2 days or on the server after 30 days. +2. Message decryption failed, because you or your contact used old database backup. +3. The connection was compromised.</source> + <target>Se voi tapahtua, kun: +1. Viestit vanhenivat lähettävässä päätelaitteessa kahden päivän päästä tai palvelimella 30 päivän kuluttua. +2. Viestin salauksen purku epäonnistui, koska sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta. +3. Yhteys vaarantui.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> + <target>Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Italian interface" xml:space="preserve"> <source>Italian interface</source> + <target>Italialainen käyttöliittymä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Japanese interface" xml:space="preserve"> + <source>Japanese interface</source> + <target>Japanilainen käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join" xml:space="preserve"> <source>Join</source> + <target>Liity</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> + <target>Liity ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Join incognito" xml:space="preserve"> <source>Join incognito</source> + <target>Liity incognito-tilassa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Joining group" xml:space="preserve"> <source>Joining group</source> + <target>Liittyy ryhmään</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep your connections" xml:space="preserve"> + <source>Keep your connections</source> + <target>Pidä kontaktisi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="KeyChain error" xml:space="preserve"> + <source>KeyChain error</source> + <target>Avainnipun virhe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Keychain error" xml:space="preserve"> <source>Keychain error</source> + <target>Avainnipun virhe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="LIVE" xml:space="preserve"> <source>LIVE</source> + <target>LIVE</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Large file!" xml:space="preserve"> <source>Large file!</source> + <target>Suuri tiedosto!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Learn more" xml:space="preserve"> + <source>Learn more</source> + <target>Lue lisää</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> + <target>Poistu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group" xml:space="preserve"> <source>Leave group</source> + <target>Poistu ryhmästä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group?" xml:space="preserve"> <source>Leave group?</source> + <target>Poistu ryhmästä?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> + <source>Let's talk in SimpleX Chat</source> + <target>Jutellaan SimpleX Chatissa</target> + <note>email subject</note> + </trans-unit> <trans-unit id="Light" xml:space="preserve"> <source>Light</source> + <target>Vaalea</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Limitations" xml:space="preserve"> <source>Limitations</source> + <target>Rajoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> + <target>Live-viesti!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Live messages" xml:space="preserve"> <source>Live messages</source> + <target>Live-viestit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local name" xml:space="preserve"> <source>Local name</source> + <target>Paikallinen nimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Local profile data only" xml:space="preserve"> <source>Local profile data only</source> + <target>Vain paikalliset profiilitiedot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Lock after" xml:space="preserve"> + <source>Lock after</source> + <target>Lukitse jälkeen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Lock mode" xml:space="preserve"> + <source>Lock mode</source> + <target>Lukitustila</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make a private connection" xml:space="preserve"> <source>Make a private connection</source> + <target>Luo yksityinen yhteys</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make one message disappear" xml:space="preserve"> + <source>Make one message disappear</source> + <target>Hävitä yksi viesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make profile private!" xml:space="preserve"> <source>Make profile private!</source> + <target>Tee profiilista yksityinen!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@).</source> + <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> + <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> + <target>Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> + <target>Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> + <target>Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?*</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> + <target>Merkitse poistetuksi kaikilta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark read" xml:space="preserve"> <source>Mark read</source> + <target>Merkitse luetuksi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark verified" xml:space="preserve"> <source>Mark verified</source> + <target>Merkitse vahvistetuksi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Markdown in messages" xml:space="preserve"> <source>Markdown in messages</source> + <target>Markdown viesteissä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> <source>Max 30 seconds, received instantly.</source> + <target>Enintään 30 sekuntia, vastaanotetaan välittömästi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member" xml:space="preserve"> <source>Member</source> + <target>Jäsen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> + <target>Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> + <target>Jäsenen rooli muutetaan muotoon "%@". Jäsen saa uuden kutsun.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> + <target>Jäsen poistetaan ryhmästä - tätä ei voi perua!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> + <target>Viestin toimitusvirhe</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve"> + <source>Message delivery receipts!</source> + <target>Viestien toimituskuittaukset!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> + <target>Viestiluonnos</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions" xml:space="preserve"> + <source>Message reactions</source> + <target>Viestireaktiot</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> + <source>Message reactions are prohibited in this chat.</source> + <target>Viestireaktiot ovat kiellettyjä tässä keskustelussa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> + <source>Message reactions are prohibited in this group.</source> + <target>Viestireaktiot ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> + <target>Viestin teksti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> + <target>Viestit</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve"> - <source>Migrating database archive...</source> + <trans-unit id="Messages & files" xml:space="preserve"> + <source>Messages & files</source> + <target>Viestit ja tiedostot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrating database archive…" xml:space="preserve"> + <source>Migrating database archive…</source> + <target>Siirretään tietokannan arkistoa…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> + <target>Siirtovirhe:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> + <target>Siirto epäonnistui. Jatka nykyisen tietokannan käyttöä napauttamalla alla **Poistu**. Ilmoita ongelmasta sovelluskehittäjille keskustelussa tai sähköpostitse [chat@simplex.chat](mailto:chat@simplex.chat).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migration is completed" xml:space="preserve"> <source>Migration is completed</source> + <target>Siirto on valmis</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Migrations: %@" xml:space="preserve"> + <source>Migrations: %@</source> + <target>Siirrot: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Moderate" xml:space="preserve"> <source>Moderate</source> + <target>Moderoi</target> <note>chat item action</note> </trans-unit> + <trans-unit id="Moderated at" xml:space="preserve"> + <source>Moderated at</source> + <target>Moderoitu klo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Moderated at: %@" xml:space="preserve"> + <source>Moderated at: %@</source> + <target>Moderoitu klo: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> + <target>Lisää parannuksia on tulossa pian!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Todennäköisesti tämä yhteys on poistettu.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> + <target>Todennäköisesti tämä kontakti on poistanut yhteyden sinuun.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> + <target>Useita keskusteluprofiileja</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> + <target>Mykistä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> + <target>Mykistetty ei-aktiivisena!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Name" xml:space="preserve"> <source>Name</source> + <target>Nimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> + <target>Verkko ja palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> + <target>Verkkoasetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Network status" xml:space="preserve"> <source>Network status</source> + <target>Verkon tila</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New Passcode" xml:space="preserve"> + <source>New Passcode</source> + <target>Uusi pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> + <target>Uusi kontaktipyyntö</target> <note>notification</note> </trans-unit> <trans-unit id="New contact:" xml:space="preserve"> <source>New contact:</source> + <target>Uusi kontakti:</target> <note>notification</note> </trans-unit> <trans-unit id="New database archive" xml:space="preserve"> <source>New database archive</source> + <target>Uusi tietokanta-arkisto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New display name" xml:space="preserve"> + <source>New display name</source> + <target>Uusi näyttönimi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> + <target>Uutta %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> + <target>Uusi jäsenrooli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="New message" xml:space="preserve"> <source>New message</source> + <target>Uusi viesti</target> <note>notification</note> </trans-unit> <trans-unit id="New passphrase…" xml:space="preserve"> <source>New passphrase…</source> + <target>Uusi tunnuslause…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No" xml:space="preserve"> <source>No</source> + <target>Ei</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No app password" xml:space="preserve"> + <source>No app password</source> + <target>Ei sovelluksen salasanaa</target> + <note>Authentication unavailable</note> + </trans-unit> <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> + <target>Kontakteja ei ole valittu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No contacts to add" xml:space="preserve"> <source>No contacts to add</source> + <target>Ei lisättäviä kontakteja</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Ei toimitustietoja</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> + <target>Ei laitetunnusta!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No filtered chats" xml:space="preserve"> + <source>No filtered chats</source> + <target>Ei suodatettuja keskusteluja</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No group!" xml:space="preserve"> <source>Group not found!</source> + <target>Ryhmää ei löydy!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No history" xml:space="preserve"> + <source>No history</source> + <target>Ei historiaa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> + <target>Ei lupaa ääniviestin tallentamiseen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> + <target>Ei vastaanotettuja tai lähetettyjä tiedostoja</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> + <target>Ilmoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Notifications are disabled!" xml:space="preserve"> <source>Notifications are disabled!</source> + <target>Ilmoitukset on poistettu käytöstä!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> + <target>Nyt järjestelmänvalvojat voivat: +- poistaa jäsenten viestit. +- poista jäsenet käytöstä ("tarkkailija" rooli)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Off" xml:space="preserve"> + <source>Off</source> + <target>Pois</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Off (Local)" xml:space="preserve"> <source>Off (Local)</source> + <target>Pois (Paikallinen)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> + <target>Ok</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> + <target>Vanha tietokanta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Old database archive" xml:space="preserve"> <source>Old database archive</source> + <target>Vanha tietokanta-arkisto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> + <target>Kertakutsulinkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> <source>Onion hosts will be required for connection. Requires enabling VPN.</source> + <target>Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> <source>Onion hosts will be used when available. Requires enabling VPN.</source> + <target>Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Onion hosts will not be used." xml:space="preserve"> <source>Onion hosts will not be used.</source> + <target>Onion-isäntiä ei käytetä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <target>Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> + <target>Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> + <source>Only group owners can enable files and media.</source> + <target>Vain ryhmän omistajat voivat sallia tiedostoja ja mediaa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> <source>Only group owners can enable voice messages.</source> + <target>Vain ryhmän omistajat voivat ottaa ääniviestit käyttöön.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can add message reactions." xml:space="preserve"> + <source>Only you can add message reactions.</source> + <target>Vain sinä voit lisätä viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> + <target>Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can make calls." xml:space="preserve"> + <source>Only you can make calls.</source> + <target>Vain sinä voit soittaa puheluita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> <source>Only you can send disappearing messages.</source> + <target>Vain sinä voit lähettää katoavia viestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can send voice messages." xml:space="preserve"> <source>Only you can send voice messages.</source> + <target>Vain sinä voit lähettää ääniviestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> + <source>Only your contact can add message reactions.</source> + <target>Vain kontaktisi voi lisätä viestireaktioita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> + <target>Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can make calls." xml:space="preserve"> + <source>Only your contact can make calls.</source> + <target>Vain kontaktisi voi soittaa puheluita.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> <source>Only your contact can send disappearing messages.</source> + <target>Vain kontaktisi voi lähettää katoavia viestejä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> <source>Only your contact can send voice messages.</source> + <target>Vain kontaktisi voi lähettää ääniviestejä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> + <target>Avaa Asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> + <target>Avaa keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open chat console" xml:space="preserve"> <source>Open chat console</source> + <target>Avaa keskustelukonsoli</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Open user profiles" xml:space="preserve"> <source>Open user profiles</source> + <target>Avaa käyttäjäprofiilit</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> <source>Open-source protocol and code – anybody can run the servers.</source> + <target>Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Opening database…" xml:space="preserve"> + <source>Opening database…</source> + <target>Avataan tietokantaa…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> + <target>Linkin avaaminen selaimessa voi heikentää yhteyden yksityisyyttä ja turvallisuutta. Epäluotetut SimpleX-linkit näkyvät punaisina.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> + <target>PING-määrä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="PING interval" xml:space="preserve"> <source>PING interval</source> + <target>PING-väli</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode" xml:space="preserve"> + <source>Passcode</source> + <target>Pääsykoodi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode changed!" xml:space="preserve"> + <source>Passcode changed!</source> + <target>Pääsykoodi vaihdettu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode entry" xml:space="preserve"> + <source>Passcode entry</source> + <target>Pääsykoodin syöttö</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode not changed!" xml:space="preserve"> + <source>Passcode not changed!</source> + <target>Pääsykoodia ei ole muutettu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Passcode set!" xml:space="preserve"> + <source>Passcode set!</source> + <target>Pääsykoodi asetettu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> + <target>Salasana näytettäväksi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste" xml:space="preserve"> <source>Paste</source> + <target>Liitä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste image" xml:space="preserve"> <source>Paste image</source> + <target>Liitä kuva</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Paste received link" xml:space="preserve"> <source>Paste received link</source> + <target>Liitä vastaanotettu linkki</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <note>No comment provided by engineer.</note> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> + <target>Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi.</target> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> + <target>Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Periodically" xml:space="preserve"> <source>Periodically</source> + <target>Ajoittain</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Permanent decryption error" xml:space="preserve"> + <source>Permanent decryption error</source> + <target>Pysyvä salauksen purkuvirhe</target> + <note>message decrypt error item</note> + </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> + <target>Pyydä kontaktiasi sallimaan ääniviestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> + <target>Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> <source>Please check your network connection with %@ and try again.</source> + <target>Tarkista verkkoyhteytesi %@:lla ja yritä uudelleen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> <source>Please check yours and your contact preferences.</source> + <target>Tarkista omasi ja kontaktin asetukset.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> + <target>Ota yhteyttä ryhmän ylläpitäjään.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> <source>Please enter correct current passphrase.</source> + <target>Anna oikea nykyinen tunnuslause.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> + <target>Anna edellinen salasana tietokannan varmuuskopion palauttamisen jälkeen. Tätä toimintoa ei voi kumota.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> + <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> + <target>Muista tai säilytä se turvallisesti - kadonnutta pääsykoodia ei voi palauttaa!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please report it to the developers." xml:space="preserve"> + <source>Please report it to the developers.</source> + <target>Ilmoita siitä kehittäjille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> <source>Please restart the app and migrate the database to enable push notifications.</source> + <target>Käynnistä sovellus uudelleen ja siirrä tietokanta push-ilmoitusten ottamiseksi käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> + <target>Säilytä tunnuslause turvallisesti, ET pääse keskusteluihin, jos kadotat sen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> + <target>Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Polish interface" xml:space="preserve"> + <source>Polish interface</source> + <target>Puolalainen käyttöliittymä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> + <target>Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen</target> <note>server test error</note> </trans-unit> <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> <source>Preserve the last message draft, with attachments.</source> + <target>Säilytä viimeinen viestiluonnos liitteineen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preset server" xml:space="preserve"> <source>Preset server</source> + <target>Esiasetettu palvelin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> + <target>Esiasetettu palvelimen osoite</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preview" xml:space="preserve"> + <source>Preview</source> + <target>Esikatselu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> + <target>Yksityisyys ja turvallisuus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> + <target>Yksityisyys uudelleen määritettynä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> + <target>Yksityiset tiedostonimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> + <target>Profiili- ja palvelinyhteydet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Profile image" xml:space="preserve"> <source>Profile image</source> + <target>Profiilikuva</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile password" xml:space="preserve"> + <source>Profile password</source> + <target>Profiilin salasana</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> + <source>Profile update will be sent to your contacts.</source> + <target>Profiilipäivitys lähetetään kontakteillesi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> + <source>Prohibit audio/video calls.</source> + <target>Estä ääni- ja videopuhelut.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> <source>Prohibit irreversible message deletion.</source> + <target>Estä peruuttamaton viestien poistaminen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit message reactions." xml:space="preserve"> + <source>Prohibit message reactions.</source> + <target>Estä viestireaktiot.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit messages reactions." xml:space="preserve"> + <source>Prohibit messages reactions.</source> + <target>Estä viestireaktiot.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> + <target>Estä suorien viestien lähettäminen jäsenille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> <source>Prohibit sending disappearing messages.</source> + <target>Estä katoavien viestien lähettäminen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending files and media." xml:space="preserve"> + <source>Prohibit sending files and media.</source> + <target>Estä tiedostojen ja median lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> <source>Prohibit sending voice messages.</source> + <target>Estä ääniviestien lähettäminen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> + <target>Suojaa sovellusnäyttö</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> + <target>Suojaa keskusteluprofiilisi salasanalla!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Protocol timeout" xml:space="preserve"> <source>Protocol timeout</source> + <target>Protokollan aikakatkaisu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protocol timeout per KB" xml:space="preserve"> + <source>Protocol timeout per KB</source> + <target>Protokollan aikakatkaisu per KB</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> + <target>Push-ilmoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> + <target>Arvioi sovellus</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="React…" xml:space="preserve"> + <source>React…</source> + <target>Reagoi…</target> + <note>chat item menu</note> + </trans-unit> <trans-unit id="Read" xml:space="preserve"> <source>Read</source> + <target>Lue</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more" xml:space="preserve"> + <source>Read more</source> + <target>Lue lisää</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> + <target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> + <target>Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> <source>Read more in our GitHub repository.</source> + <target>Lue lisää GitHub-tietovarastostamme.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> + <target>Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Kuittaukset pois käytöstä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at" xml:space="preserve"> + <source>Received at</source> + <target>Vastaanotettu klo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at: %@" xml:space="preserve"> + <source>Received at: %@</source> + <target>Vastaanotettu klo: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Received file event" xml:space="preserve"> <source>Received file event</source> + <target>Tiedoston vastaanottotapahtuma</target> <note>notification</note> </trans-unit> + <trans-unit id="Received message" xml:space="preserve"> + <source>Received message</source> + <target>Vastaanotettu viesti</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> + <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving file will be stopped." xml:space="preserve"> + <source>Receiving file will be stopped.</source> + <target>Tiedoston vastaanotto pysäytetään.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Receiving via" xml:space="preserve"> <source>Receiving via</source> + <target>Vastaanotto kautta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> + <target>Vastaanottajat näkevät päivitykset, kun kirjoitat niitä.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>Yhdistä kaikki yhdistetyt palvelimet uudelleen pakottaaksesi viestin toimituksen. Tämä käyttää ylimääräistä liikennettä.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect servers?" xml:space="preserve"> + <source>Reconnect servers?</source> + <target>Yhdistä palvelimet uudelleen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at" xml:space="preserve"> + <source>Record updated at</source> + <target>Tietue päivitetty klo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at: %@" xml:space="preserve"> + <source>Record updated at: %@</source> + <target>Tietue päivitetty klo: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Reduced battery usage" xml:space="preserve"> <source>Reduced battery usage</source> + <target>Pienempi akun käyttö</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> + <target>Hylkää</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> + <target>Hylkää (lähettäjälle EI ilmoiteta)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reject contact request" xml:space="preserve"> <source>Reject contact request</source> + <target>Hylkää yhteyspyyntö</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> <source>Relay server is only used if necessary. Another party can observe your IP address.</source> + <target>Välityspalvelinta käytetään vain tarvittaessa. Toinen osapuoli voi tarkkailla IP-osoitettasi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> <source>Relay server protects your IP address, but it can observe the duration of the call.</source> + <target>Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove" xml:space="preserve"> <source>Remove</source> + <target>Poista</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> + <target>Poista jäsen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove member?" xml:space="preserve"> <source>Remove member?</source> + <target>Poista jäsen?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> <source>Remove passphrase from keychain?</source> + <target>Poista tunnuslause avainnipusta?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate" xml:space="preserve"> + <source>Renegotiate</source> + <target>Neuvottele uudelleen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption" xml:space="preserve"> + <source>Renegotiate encryption</source> + <target>Uudelleenneuvottele salaus</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption?" xml:space="preserve"> + <source>Renegotiate encryption?</source> + <target>Uudelleenneuvottele salaus?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> + <target>Vastaa</target> <note>chat item action</note> </trans-unit> <trans-unit id="Required" xml:space="preserve"> <source>Required</source> + <target>Pakollinen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset" xml:space="preserve"> <source>Reset</source> + <target>Oletustilaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> + <target>Oletusvärit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> + <target>Palauta oletusasetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> + <target>Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> <source>Restart the app to use imported chat database</source> + <target>Käynnistä sovellus uudelleen käyttääksesi tuotua keskustelujen-tietokantaa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore" xml:space="preserve"> <source>Restore</source> + <target>Palauta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database backup" xml:space="preserve"> <source>Restore database backup</source> + <target>Palauta tietokannan varmuuskopio</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database backup?" xml:space="preserve"> <source>Restore database backup?</source> + <target>Palauta tietokannan varmuuskopio?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Restore database error" xml:space="preserve"> <source>Restore database error</source> + <target>Virhe tietokannan palauttamisessa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> + <target>Paljasta</target> <note>chat item action</note> </trans-unit> <trans-unit id="Revert" xml:space="preserve"> <source>Revert</source> + <target>Palauta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Revoke" xml:space="preserve"> + <source>Revoke</source> + <target>Peruuta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Revoke file" xml:space="preserve"> + <source>Revoke file</source> + <target>Peruuta tiedosto</target> + <note>cancel file action</note> + </trans-unit> + <trans-unit id="Revoke file?" xml:space="preserve"> + <source>Revoke file?</source> + <target>Peruuta tiedosto?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Role" xml:space="preserve"> <source>Role</source> + <target>Rooli</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Run chat" xml:space="preserve"> <source>Run chat</source> + <target>Käynnistä chat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SMP servers" xml:space="preserve"> <source>SMP servers</source> + <target>SMP-palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save" xml:space="preserve"> <source>Save</source> + <target>Tallenna</target> <note>chat item action</note> </trans-unit> <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> + <target>Tallenna (ja ilmoita kontakteille)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> + <target>Tallenna ja ilmoita kontaktille</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> + <target>Tallenna ja ilmoita ryhmän jäsenille</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> + <target>Tallenna ja päivitä ryhmäprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save archive" xml:space="preserve"> <source>Save archive</source> + <target>Tallenna arkisto</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save auto-accept settings" xml:space="preserve"> + <source>Save auto-accept settings</source> + <target>Tallenna automaattisen hyväksynnän asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> + <target>Tallenna ryhmäprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> + <target>Tallenna tunnuslause ja avaa keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> <source>Save passphrase in Keychain</source> + <target>Tallenna tunnuslause Avainnippuun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> + <target>Tallenna asetukset?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> + <target>Tallenna profiilin salasana</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers" xml:space="preserve"> <source>Save servers</source> + <target>Tallenna palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> + <target>Tallenna palvelimet?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save settings?" xml:space="preserve"> + <source>Save settings?</source> + <target>Tallenna asetukset?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> + <target>Tallenna tervetuloviesti?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> + <target>Tallennetut WebRTC ICE -palvelimet poistetaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> + <target>Skannaa QR-koodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan code" xml:space="preserve"> <source>Scan code</source> + <target>Skannaa koodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> <source>Scan security code from your contact's app.</source> + <target>Skannaa turvakoodi kontaktisi sovelluksesta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan server QR code" xml:space="preserve"> <source>Scan server QR code</source> + <target>Skannaa palvelimen QR-koodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Search" xml:space="preserve"> <source>Search</source> + <target>Haku</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> + <target>Turvallinen jono</target> <note>server test step</note> </trans-unit> <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> + <target>Turvallisuusarviointi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Security code" xml:space="preserve"> <source>Security code</source> + <target>Turvakoodi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Select" xml:space="preserve"> + <source>Select</source> + <target>Valitse</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct" xml:space="preserve"> + <source>Self-destruct</source> + <target>Itsetuho</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode" xml:space="preserve"> + <source>Self-destruct passcode</source> + <target>Itsetuhoutuva pääsykoodi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> + <source>Self-destruct passcode changed!</source> + <target>Itsetuhoutuva pääsykoodi vaihdettu!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> + <source>Self-destruct passcode enabled!</source> + <target>Itsetuhoutuva pääsykoodi käytössä!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send" xml:space="preserve"> <source>Send</source> + <target>Lähetä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> <source>Send a live message - it will update for the recipient(s) as you type it</source> + <target>Lähetä live-viesti - se päivittyy vastaanottajille, kun kirjoitat sitä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send delivery receipts to" xml:space="preserve"> + <source>Send delivery receipts to</source> + <target>Lähetä toimituskuittaukset vastaanottajalle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send direct message" xml:space="preserve"> <source>Send direct message</source> + <target>Lähetä yksityisviesti</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send disappearing message" xml:space="preserve"> + <source>Send disappearing message</source> + <target>Lähetä katoava viesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> + <target>Lähetä linkkien esikatselu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send live message" xml:space="preserve"> <source>Send live message</source> + <target>Lähetä live-viesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> + <target>Lähetys ilmoitukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send notifications:" xml:space="preserve"> <source>Send notifications:</source> + <target>Lähetys ilmoitukset:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send questions and ideas" xml:space="preserve"> <source>Send questions and ideas</source> + <target>Lähetä kysymyksiä ja ideoita</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send receipts" xml:space="preserve"> + <source>Send receipts</source> + <target>Lähetä kuittaukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> <source>Send them from gallery or custom keyboards.</source> + <target>Lähetä ne galleriasta tai mukautetuista näppäimistöistä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> + <target>Lähettäjä peruutti tiedoston siirron.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> + <target>Lähettäjä on saattanut poistaa yhteyspyynnön.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille näkyvissä keskusteluprofiileissa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending file will be stopped." xml:space="preserve"> + <source>Sending file will be stopped.</source> + <target>Tiedoston lähettäminen lopetetaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is disabled for %lld contacts</source> + <target>Kuittauksien lähettäminen ei ole käytössä %lld kontakteille</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Kuittien lähettäminen ei ole käytössä %lld ryhmille</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is enabled for %lld contacts</source> + <target>Kuittauksien lähettäminen on käytössä %lld kontakteille</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Kuittauksien lähettäminen on käytössä %lld ryhmille</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> + <target>Lähetetään kautta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sent at" xml:space="preserve"> + <source>Sent at</source> + <target>Lähetetty klo</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent at: %@" xml:space="preserve"> + <source>Sent at: %@</source> + <target>Lähetetty klo: %@</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> + <target>Lähetetty tiedosto tapahtuma</target> <note>notification</note> </trans-unit> + <trans-unit id="Sent message" xml:space="preserve"> + <source>Sent message</source> + <target>Lähetetty viesti</target> + <note>message info title</note> + </trans-unit> <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> + <target>Lähetetyt viestit poistetaan asetetun ajan kuluttua.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> + <target>Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> + <source>Server requires authorization to upload, check password</source> + <target>Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana</target> <note>server test error</note> </trans-unit> <trans-unit id="Server test failed!" xml:space="preserve"> <source>Server test failed!</source> + <target>Palvelintesti epäonnistui!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> + <target>Palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set 1 day" xml:space="preserve"> <source>Set 1 day</source> + <target>Aseta 1 päivä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> + <target>Aseta kontaktin nimi…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> + <target>Aseta ryhmän asetukset</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set it instead of system authentication." xml:space="preserve"> + <source>Set it instead of system authentication.</source> + <target>Aseta se järjestelmän todennuksen sijaan.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passcode" xml:space="preserve"> + <source>Set passcode</source> + <target>Aseta pääsykoodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> + <target>Aseta tunnuslause vientiä varten</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set the message shown to new members!" xml:space="preserve"> <source>Set the message shown to new members!</source> + <target>Aseta uusille jäsenille näytettävä viesti!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> <source>Set timeouts for proxy/VPN</source> + <target>Aseta aikakatkaisut välityspalvelimelle/VPN:lle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Settings" xml:space="preserve"> <source>Settings</source> + <target>Asetukset</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share" xml:space="preserve"> <source>Share</source> + <target>Jaa</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Share invitation link" xml:space="preserve"> - <source>Share invitation link</source> + <trans-unit id="Share 1-time link" xml:space="preserve"> + <source>Share 1-time link</source> + <target>Jaa kertakäyttölinkki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address" xml:space="preserve"> + <source>Share address</source> + <target>Jaa osoite</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address with contacts?" xml:space="preserve"> + <source>Share address with contacts?</source> + <target>Jaa osoite kontakteille?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share link" xml:space="preserve"> <source>Share link</source> + <target>Jaa linkki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Share one-time invitation link" xml:space="preserve"> <source>Share one-time invitation link</source> + <target>Jaa kertakutsulinkki</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show QR code" xml:space="preserve"> - <source>Show QR code</source> + <trans-unit id="Share with contacts" xml:space="preserve"> + <source>Share with contacts</source> + <target>Jaa kontaktien kanssa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> + <target>Näytä puhelut puhelinhistoriassa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show developer options" xml:space="preserve"> + <source>Show developer options</source> + <target>Näytä kehittäjävaihtoehdot</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Näytä viimeiset viestit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> + <target>Näytä esikatselu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show:" xml:space="preserve"> + <source>Show:</source> + <target>Näytä:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Address" xml:space="preserve"> + <source>SimpleX Address</source> + <target>SimpleX-osoite</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> + <target>Trail of Bits on tarkastanut SimpleX Chatin tietoturvan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock" xml:space="preserve"> <source>SimpleX Lock</source> + <target>SimpleX Lock</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock mode" xml:space="preserve"> + <source>SimpleX Lock mode</source> + <target>SimpleX Lock -tila</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> + <source>SimpleX Lock not enabled!</source> + <target>SimpleX Lock ei ole käytössä!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> <source>SimpleX Lock turned on</source> + <target>SimpleX Lock päällä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address" xml:space="preserve"> + <source>SimpleX address</source> + <target>SimpleX-osoite</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> + <target>SimpleX-yhteystiedot</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> <source>SimpleX encrypted message or connection event</source> + <target>SimpleX-salattu viesti tai yhteystapahtuma</target> <note>notification</note> </trans-unit> <trans-unit id="SimpleX group link" xml:space="preserve"> <source>SimpleX group link</source> + <target>SimpleX-ryhmän linkki</target> <note>simplex link type</note> </trans-unit> <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> + <target>SimpleX-linkit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> <source>SimpleX one-time invitation</source> + <target>SimpleX-kertakutsu</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> + <target>Ohita</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Skipped messages" xml:space="preserve"> <source>Skipped messages</source> + <target>Ohitetut viestit</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Pienryhmät (max 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> + <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> + <target>Joku</target> <note>notification title</note> </trans-unit> <trans-unit id="Start a new chat" xml:space="preserve"> <source>Start a new chat</source> + <target>Aloita uusi keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start chat" xml:space="preserve"> <source>Start chat</source> + <target>Aloita keskustelu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> + <target>Aloita siirto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> + <target>Lopeta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop SimpleX" xml:space="preserve"> <source>Stop SimpleX</source> + <target>Lopeta SimpleX</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> <source>Stop chat to enable database actions</source> + <target>Pysäytä keskustelu tietokantatoimien mahdollistamiseksi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> + <target>Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Stop chat?" xml:space="preserve"> <source>Stop chat?</source> + <target>Lopeta keskustelu?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop file" xml:space="preserve"> + <source>Stop file</source> + <target>Pysäytä tiedosto</target> + <note>cancel file action</note> + </trans-unit> + <trans-unit id="Stop receiving file?" xml:space="preserve"> + <source>Stop receiving file?</source> + <target>Lopeta tiedoston vastaanottaminen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sending file?" xml:space="preserve"> + <source>Stop sending file?</source> + <target>Lopeta tiedoston lähettäminen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sharing" xml:space="preserve"> + <source>Stop sharing</source> + <target>Lopeta jakaminen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sharing address?" xml:space="preserve"> + <source>Stop sharing address?</source> + <target>Lopeta osoitteen jakaminen?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Submit" xml:space="preserve"> + <source>Submit</source> + <target>Lähetä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> + <target>SimpleX Chat tuki</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="System" xml:space="preserve"> <source>System</source> + <target>Järjestelmä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="System authentication" xml:space="preserve"> + <source>System authentication</source> + <target>Järjestelmän todennus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> + <target>TCP-yhteyden aikakatkaisu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> + <target>TCP_KEEPCNT</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> <source>TCP_KEEPIDLE</source> + <target>TCP_KEEPIDLE</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> <source>TCP_KEEPINTVL</source> + <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> + <target>Ota kuva</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> + <target>Napauta painiketta </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> + <target>Aktivoi profiili napauttamalla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> <source>Tap to join</source> + <target>Liity napauttamalla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join incognito" xml:space="preserve"> <source>Tap to join incognito</source> + <target>Napauta liittyäksesi incognito-tilassa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to start a new chat" xml:space="preserve"> <source>Tap to start a new chat</source> + <target>Aloita uusi keskustelu napauttamalla</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> + <target>Testi epäonnistui vaiheessa %@.</target> <note>server test failure</note> </trans-unit> <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> + <target>Testipalvelin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Test servers" xml:space="preserve"> <source>Test servers</source> + <target>Testipalvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> + <target>Testit epäonnistuivat!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> + <target>Kiitos SimpleX Chatin asentamisesta!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Kiitos käyttäjille - [osallistu Weblaten avulla](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> <source>Thanks to the users – contribute via Weblate!</source> + <target>Kiitokset käyttäjille – osallistu Weblaten kautta!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> <source>The 1st platform without any user identifiers – private by design.</source> + <target>Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> + <source>The ID of the next message is incorrect (less or equal to the previous). +It can happen because of some bug or when the connection is compromised.</source> + <target>Seuraavan viestin tunnus on väärä (pienempi tai yhtä suuri kuin edellisen). +Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> + <target>Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> + <target>Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> + <target>Hyväksymäsi yhteys peruuntuu!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> <source>The contact you shared this link with will NOT be able to connect!</source> + <target>Kontakti, jolle jaoit tämän linkin, EI voi muodostaa yhteyttä!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> <source>The created archive is available via app Settings / Database / Old database archive.</source> + <target>Luotu arkisto on käytettävissä sovelluksen Asetukset / Tietokanta / Vanha tietokanta-arkisto kautta.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> + <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> <source>The group is fully decentralized – it is visible only to the members.</source> + <target>Ryhmä on täysin hajautettu - se näkyy vain jäsenille.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The hash of the previous message is different." xml:space="preserve"> + <source>The hash of the previous message is different.</source> + <target>Edellisen viestin tarkiste on erilainen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be deleted for all members." xml:space="preserve"> <source>The message will be deleted for all members.</source> + <target>Viesti poistetaan kaikilta jäseniltä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> <source>The message will be marked as moderated for all members.</source> + <target>Viesti merkitään moderoiduksi kaikille jäsenille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The next generation of private messaging" xml:space="preserve"> <source>The next generation of private messaging</source> + <target>Seuraavan sukupolven yksityisviestit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> + <target>Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> <source>The profile is only shared with your contacts.</source> + <target>Profiili jaetaan vain kontaktiesi kanssa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> + <source>The second tick we missed! ✅</source> + <target>Toinen kuittaus, joka uupui! ✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> <source>The sender will NOT be notified</source> + <target>Lähettäjälle EI ilmoiteta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> + <target>Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Theme" xml:space="preserve"> <source>Theme</source> + <target>Teema</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="There should be at least one user profile." xml:space="preserve"> <source>There should be at least one user profile.</source> + <target>Käyttäjäprofiileja tulee olla vähintään yksi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> <source>There should be at least one visible user profile.</source> + <target>Näkyviä käyttäjäprofiileja tulee olla vähintään yksi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> + <source>These settings are for your current profile **%@**.</source> + <target>Nämä asetukset koskevat nykyistä profiiliasi **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> + <target>Tätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> + <target>Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> + <target>Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve"> - <source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> + <target>Tätä ryhmää ei enää ole olemassa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> + <target>Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> + <target>Voit esittää kysymyksiä ja saada päivityksiä:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> + <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> + <source>To connect, your contact can scan QR code or use the link in the app.</source> + <target>Kontaktisi voi muodostaa yhteyden skannaamalla QR-koodin tai käyttämällä sovelluksessa olevaa linkkiä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> + <target>Uuden yhteyden luominen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <target>Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> <source>To protect timezone, image/voice files use UTC.</source> + <target>Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> + <target>Suojaa tietosi ottamalla SimpleX Lock käyttöön. +Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> + <target>Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> + <target>Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> + <target>Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> + <target>Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> + <target>Kuljetuksen eristäminen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> + <target>Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact.</source> + <target>Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> + <target>Sammuta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn off notifications?" xml:space="preserve"> <source>Turn off notifications?</source> + <target>Kytke ilmoitukset pois päältä?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> + <target>Kytke päälle</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unable to record voice message" xml:space="preserve"> <source>Unable to record voice message</source> + <target>Ääniviestiä ei voi tallentaa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> - <note>No comment provided by engineer.</note> + <target>Odottamaton virhe: %@</target> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> + <target>Odottamaton siirtotila</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unfav." xml:space="preserve"> + <source>Unfav.</source> + <target>Epäsuotuisa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> + <target>Näytä</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide chat profile" xml:space="preserve"> + <source>Unhide chat profile</source> + <target>Näytä keskusteluprofiili</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide profile" xml:space="preserve"> + <source>Unhide profile</source> + <target>Näytä profiili</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unit" xml:space="preserve"> + <source>Unit</source> + <target>Yksikkö</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unknown caller" xml:space="preserve"> <source>Unknown caller</source> + <target>Tuntematon soittaja</target> <note>callkit banner</note> </trans-unit> <trans-unit id="Unknown database error: %@" xml:space="preserve"> <source>Unknown database error: %@</source> + <target>Tuntematon tietokantavirhe: %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unknown error" xml:space="preserve"> <source>Unknown error</source> + <target>Tuntematon virhe</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> + <target>Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> + <target>Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä. +Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> <source>Unlock</source> + <target>Avaa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlock app" xml:space="preserve"> + <source>Unlock app</source> + <target>Avaa sovellus</target> <note>authentication reason</note> </trans-unit> <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> + <target>Poista mykistys</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> + <target>Lukematon</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update" xml:space="preserve"> <source>Update</source> + <target>Päivitä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> <source>Update .onion hosts setting?</source> + <target>Päivitä .onion-isäntien asetus?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> + <target>Päivitä tietokannan tunnuslause</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update network settings?" xml:space="preserve"> <source>Update network settings?</source> + <target>Päivitä verkkoasetukset?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Update transport isolation mode?" xml:space="preserve"> <source>Update transport isolation mode?</source> + <target>Päivitä kuljetuksen eristystila?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> <source>Updating settings will re-connect the client to all servers.</source> + <target>Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> <source>Updating this setting will re-connect the client to all servers.</source> + <target>Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Upgrade and open chat" xml:space="preserve"> + <source>Upgrade and open chat</source> + <target>Päivitä ja avaa keskustelu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload file" xml:space="preserve"> + <source>Upload file</source> + <target>Lataa tiedosto</target> + <note>server test step</note> + </trans-unit> <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> + <target>Käytä .onion-isäntiä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> + <target>Käytä SimpleX Chat palvelimia?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> + <target>Käytä chattia</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Käytä nykyistä profiilia</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> + <target>Käytä uusiin yhteyksiin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use iOS call interface" xml:space="preserve"> <source>Use iOS call interface</source> + <target>Käytä iOS:n puhelujen käyttöliittymää</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Käytä uutta incognito-profiilia</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> + <target>Käytä palvelinta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="User profile" xml:space="preserve"> <source>User profile</source> + <target>Käyttäjäprofiili</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> <source>Using .onion hosts requires compatible VPN provider.</source> + <target>.onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> + <target>Käyttää SimpleX Chat -palvelimia.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify connection security" xml:space="preserve"> <source>Verify connection security</source> + <target>Tarkista yhteyden suojaus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> + <target>Tarkista turvakoodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Via browser" xml:space="preserve"> <source>Via browser</source> + <target>Selaimella</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video call" xml:space="preserve"> <source>Video call</source> + <target>Videopuhelu</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> + <source>Video will be received when your contact completes uploading it.</source> + <target>Video vastaanotetaan, kun kontaktisi on ladannut sen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>Video will be received when your contact is online, please wait or check later!</source> + <target>Video vastaanotetaan, kun kontaktisi on online-tilassa, odota tai tarkista myöhemmin!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> + <source>Videos and files up to 1gb</source> + <target>Videot ja tiedostot 1 Gt asti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> + <target>Näytä turvakoodi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> + <target>Ääniviestit</target> <note>chat feature</note> </trans-unit> <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> <source>Voice messages are prohibited in this chat.</source> + <target>Ääniviestit ovat kiellettyjä tässä keskustelussa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> <source>Voice messages are prohibited in this group.</source> + <target>Ääniviestit ovat kiellettyjä tässä ryhmässä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> + <target>Ääniviestit kielletty!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Voice message…" xml:space="preserve"> <source>Voice message…</source> + <target>Ääniviesti…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for file" xml:space="preserve"> <source>Waiting for file</source> + <target>Odottaa tiedostoa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Waiting for image" xml:space="preserve"> <source>Waiting for image</source> + <target>Odottaa kuvaa</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for video" xml:space="preserve"> + <source>Waiting for video</source> + <target>Odottaa videota</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> + <source>Warning: you may lose some data!</source> + <target>Varoitus: saatat menettää joitain tietoja!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="WebRTC ICE servers" xml:space="preserve"> <source>WebRTC ICE servers</source> + <target>WebRTC ICE -palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Welcome %@!" xml:space="preserve"> <source>Welcome %@!</source> + <target>Tervetuloa %@!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Welcome message" xml:space="preserve"> <source>Welcome message</source> + <target>Tervetuloviesti</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> + <target>Uusimmat</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When available" xml:space="preserve"> <source>When available</source> + <target>Kun saatavilla</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> + <source>When people request to connect, you can accept or reject it.</source> + <target>Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> + <target>Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> + <target>Valinnaisella tervetuloviestillä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> + <target>Väärä tietokannan tunnuslause</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> + <target>Väärä tunnuslause!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="XFTP servers" xml:space="preserve"> + <source>XFTP servers</source> + <target>XFTP-palvelimet</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You" xml:space="preserve"> <source>You</source> + <target>Sinä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You accepted connection" xml:space="preserve"> <source>You accepted connection</source> + <target>Hyväksyit yhteyden</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You allow" xml:space="preserve"> <source>You allow</source> + <target>Sallit</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> + <target>Sinulla on jo keskusteluprofiili samalla näyttönimellä. Valitse toinen nimi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are already connected to %@." xml:space="preserve"> <source>You are already connected to %@.</source> + <target>Olet jo muodostanut yhteyden %@:n kanssa.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <source>You are connected to the server used to receive messages from this contact.</source> + <target>Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You are invited to group" xml:space="preserve"> <source>You are invited to group</source> + <target>Sinut on kutsuttu ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> + <target>Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> + <target>Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can hide or mute a user profile - swipe it to the right. SimpleX Lock must be enabled." xml:space="preserve"> - <source>You can hide or mute a user profile - swipe it to the right. -SimpleX Lock must be enabled.</source> + <trans-unit id="You can create it later" xml:space="preserve"> + <source>You can create it later</source> + <target>Voit luoda sen myöhemmin</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable later via Settings" xml:space="preserve"> + <source>You can enable later via Settings</source> + <target>Voit ottaa käyttöön myöhemmin asetusten kautta</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> + <source>You can enable them later via app Privacy & Security settings.</source> + <target>Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> + <source>You can hide or mute a user profile - swipe it to the right.</source> + <target>Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can now send messages to %@" xml:space="preserve"> <source>You can now send messages to %@</source> + <target>Voit nyt lähettää viestejä %@:lle</target> <note>notification body</note> </trans-unit> <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> + <target>Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> + <target>Voit jakaa linkin tai QR-koodin - kuka tahansa voi liittyä ryhmään. Et menetä ryhmän jäseniä, jos poistat sen myöhemmin.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve"> - <source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source> + <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> + <source>You can share this address with your contacts to let them connect with **%@**.</source> + <target>Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> + <source>You can share your address as a link or QR code - anybody can connect to you.</source> + <target>Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> + <target>Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> + <source>You can turn on SimpleX Lock via Settings.</source> + <target>Voit ottaa SimpleX Lockin käyttöön Asetusten kautta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> <source>You can use markdown to format messages:</source> + <target>Voit käyttää markdownia viestien muotoiluun:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> + <target>Et voi lähettää viestejä!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> + <target>Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> + <target>Sinua ei voitu todentaa; yritä uudelleen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have no chats" xml:space="preserve"> <source>You have no chats</source> + <target>Sinulla ei ole keskusteluja</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> + <target>Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Kutsuit kontaktin</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group" xml:space="preserve"> <source>You joined this group</source> + <target>Liityit tähän ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> <source>You joined this group. Connecting to inviting group member.</source> + <target>Liityit tähän ryhmään. Muodostetaan yhteyttä ryhmän jäsenten kutsumiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> + <target>Sinun tulee käyttää keskustelujen-tietokannan uusinta versiota AINOSTAAN yhdessä laitteessa, muuten saatat lakata vastaanottamasta viestejä joiltakin kontakteilta.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> + <target>Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You rejected group invitation" xml:space="preserve"> <source>You rejected group invitation</source> + <target>Hylkäsit ryhmäkutsun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You sent group invitation" xml:space="preserve"> <source>You sent group invitation</source> + <target>Lähetit ryhmäkutsun</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> + <target>Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <source>You will be connected when your connection request is accepted, please wait or check later!</source> + <target>Sinut yhdistetään, kun yhteyspyyntösi on hyväksytty, odota tai tarkista myöhemmin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when your contact's device is online, please wait or check later!</source> + <target>Sinut yhdistetään, kun kontaktisi laite on online-tilassa, odota tai tarkista myöhemmin!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> + <target>Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> <source>You will join a group this link refers to and connect to its group members.</source> + <target>Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <source>You will still receive calls and notifications from muted profiles when they are active.</source> + <target>Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> + <target>Et enää saa viestejä tästä ryhmästä. Keskusteluhistoria säilytetään.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> + <source>You won't lose your contacts if you later delete your address.</source> + <target>Et menetä kontaktejasi, jos poistat osoitteesi myöhemmin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> + <target>Yrität kutsua kontaktia, jonka kanssa olet jakanut inkognito-profiilin, ryhmään, jossa käytät pääprofiiliasi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> + <target>Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your %@ servers" xml:space="preserve"> + <source>Your %@ servers</source> + <target>%@-palvelimesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> + <target>ICE-palvelimesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your SMP servers" xml:space="preserve"> <source>Your SMP servers</source> + <target>SMP-palvelimesi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SimpleX contact address" xml:space="preserve"> - <source>Your SimpleX contact address</source> + <trans-unit id="Your SimpleX address" xml:space="preserve"> + <source>Your SimpleX address</source> + <target>SimpleX-osoitteesi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your XFTP servers" xml:space="preserve"> + <source>Your XFTP servers</source> + <target>XFTP-palvelimesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> + <target>Puhelusi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database" xml:space="preserve"> <source>Your chat database</source> + <target>Keskustelut-tietokantasi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> + <target>Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> <source>Your chat profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> + <target>Keskusteluprofiilisi lähetetään ryhmän jäsenille</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chats" xml:space="preserve"> - <source>Your chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact address" xml:space="preserve"> - <source>Your contact address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact can scan it from the app." xml:space="preserve"> - <source>Your contact can scan it from the app.</source> + <target>Keskusteluprofiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> <source>Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link).</source> + <target>Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa. +Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> + <target>Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> <source>Your contacts can allow full message deletion.</source> + <target>Kontaktisi voivat sallia viestien täydellisen poistamisen.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> + <source>Your contacts in SimpleX will see it. +You can change it in Settings.</source> + <target>Kontaktisi SimpleX:ssä näkevät sen. +Voit muuttaa sitä Asetuksista.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts will remain connected." xml:space="preserve"> + <source>Your contacts will remain connected.</source> + <target>Kontaktisi pysyvät yhdistettyinä.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> + <target>Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your current profile" xml:space="preserve"> <source>Your current profile</source> + <target>Nykyinen profiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your preferences" xml:space="preserve"> <source>Your preferences</source> + <target>Asetuksesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your privacy" xml:space="preserve"> <source>Your privacy</source> + <target>Yksityisyytesi</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Profiilisi **%@** jaetaan.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> + <target>Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa. +SimpleX-palvelimet eivät näe profiiliasi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> + <target>Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> + <target>Satunnainen profiilisi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your server" xml:space="preserve"> <source>Your server</source> + <target>Palvelimesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> + <target>Palvelimesi osoite</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> + <target>Asetuksesi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> + <target>[Osallistu](https://github.com/simplex-chat/simplex-chat#contribute)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> <source>[Send us email](mailto:chat@simplex.chat)</source> + <target>[Lähetä meille sähköpostia](mailto:chat@simplex.chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> + <target>[Tähti GitHubissa](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="_italic_" xml:space="preserve"> <source>\_italic_</source> + <target>\_italic_</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="`a + b`" xml:space="preserve"> <source>\`a + b`</source> + <target>\`a + b`</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="above, then choose:" xml:space="preserve"> <source>above, then choose:</source> + <target>edellä, valitse sitten:</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="accepted call" xml:space="preserve"> <source>accepted call</source> + <target>hyväksytty puhelu</target> <note>call status</note> </trans-unit> <trans-unit id="admin" xml:space="preserve"> <source>admin</source> + <target>ylläpitäjä</target> <note>member role</note> </trans-unit> + <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> + <source>agreeing encryption for %@…</source> + <target>salauksesta sovitaan %@:lle…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="agreeing encryption…" xml:space="preserve"> + <source>agreeing encryption…</source> + <target>hyväksyy salausta…</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="always" xml:space="preserve"> <source>always</source> + <target>aina</target> <note>pref value</note> </trans-unit> <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> + <target>äänipuhelu (ei e2e-salattu)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> <source>bad message ID</source> + <target>virheellinen viestin tunniste</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="bad message hash" xml:space="preserve"> <source>bad message hash</source> + <target>virheellinen viestin tarkiste</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="bold" xml:space="preserve"> <source>bold</source> + <target>lihavoitu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="call error" xml:space="preserve"> <source>call error</source> + <target>soittovirhe</target> <note>call status</note> </trans-unit> <trans-unit id="call in progress" xml:space="preserve"> <source>call in progress</source> + <target>puhelu käynnissä</target> <note>call status</note> </trans-unit> <trans-unit id="calling…" xml:space="preserve"> <source>calling…</source> + <target>soittaa…</target> <note>call status</note> </trans-unit> <trans-unit id="cancelled %@" xml:space="preserve"> <source>cancelled %@</source> + <target>peruutettu %@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> + <target>muuttunut osoite sinulle</target> <note>chat item text</note> </trans-unit> <trans-unit id="changed role of %@ to %@" xml:space="preserve"> <source>changed role of %1$@ to %2$@</source> + <target>%1$@:n roolin muuttui %2$@:ksi</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="changed your role to %@" xml:space="preserve"> <source>changed your role to %@</source> + <target>roolisi muuttui %@:ksi</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="changing address for %@..." xml:space="preserve"> - <source>changing address for %@...</source> + <trans-unit id="changing address for %@…" xml:space="preserve"> + <source>changing address for %@…</source> + <target>osoitteen muuttaminen %@:lle…</target> <note>chat item text</note> </trans-unit> - <trans-unit id="changing address..." xml:space="preserve"> - <source>changing address...</source> + <trans-unit id="changing address…" xml:space="preserve"> + <source>changing address…</source> + <target>muuttamassa osoitetta…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> <source>colored</source> + <target>värillinen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="complete" xml:space="preserve"> <source>complete</source> + <target>valmis</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> <source>connect to SimpleX Chat developers.</source> + <target>ole yhteydessä SimpleX Chat -kehittäjiin.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connected" xml:space="preserve"> <source>connected</source> + <target>yhdistetty</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> + <target>yhdistää</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (accepted)" xml:space="preserve"> <source>connecting (accepted)</source> + <target>yhdistäminen (hyväksytty)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (announced)" xml:space="preserve"> <source>connecting (announced)</source> + <target>yhdistäminen (ilmoitettu)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (introduced)" xml:space="preserve"> <source>connecting (introduced)</source> + <target>yhdistäminen (esitelty)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> <source>connecting (introduction invitation)</source> + <target>yhdistäminen (esittelykutsu)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="connecting call" xml:space="preserve"> <source>connecting call…</source> + <target>yhdistää puhelun…</target> <note>call status</note> </trans-unit> <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> + <target>yhdistää…</target> <note>chat list item title</note> </trans-unit> <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> + <target>yhteys luotu</target> <note>chat list item title (it should not be shown</note> </trans-unit> <trans-unit id="connection:%@" xml:space="preserve"> <source>connection:%@</source> + <target>yhteys:%@</target> <note>connection information</note> </trans-unit> <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> + <target>kontaktilla on e2e-salaus</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="contact has no e2e encryption" xml:space="preserve"> <source>contact has no e2e encryption</source> + <target>kontaktilla ei ole e2e-salausta</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="creator" xml:space="preserve"> <source>creator</source> + <target>luoja</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="custom" xml:space="preserve"> + <source>custom</source> + <target>mukautettu</target> + <note>dropdown time picker choice</note> + </trans-unit> + <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> + <source>database version is newer than the app, but no down migration for: %@</source> + <target>tietokantaversio on uudempi kuin sovellus, mutta ei alaspäin siirtymistä varten: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="days" xml:space="preserve"> + <source>days</source> + <target>päivää</target> + <note>time unit</note> + </trans-unit> <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> + <target>oletusarvo (%@)</target> <note>pref value</note> </trans-unit> + <trans-unit id="default (no)" xml:space="preserve"> + <source>default (no)</source> + <target>oletusarvo (ei)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="default (yes)" xml:space="preserve"> + <source>default (yes)</source> + <target>oletusarvo (kyllä)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="deleted" xml:space="preserve"> <source>deleted</source> + <target>poistettu</target> <note>deleted chat item</note> </trans-unit> <trans-unit id="deleted group" xml:space="preserve"> <source>deleted group</source> + <target>poistettu ryhmä</target> <note>rcv group event chat item</note> </trans-unit> + <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> + <source>different migration in the app/database: %@ / %@</source> + <target>eri siirtyminen sovelluksessa/tietokannassa: %@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="direct" xml:space="preserve"> <source>direct</source> + <target>suora</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>ei käytössä</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> + <target>päällekkäinen viesti</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> + <target>e2e-salattu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="enabled" xml:space="preserve"> <source>enabled</source> + <target>käytössä</target> <note>enabled status</note> </trans-unit> <trans-unit id="enabled for contact" xml:space="preserve"> <source>enabled for contact</source> + <target>käytössä kontaktille</target> <note>enabled status</note> </trans-unit> <trans-unit id="enabled for you" xml:space="preserve"> <source>enabled for you</source> + <target>käytössä sinulle</target> <note>enabled status</note> </trans-unit> + <trans-unit id="encryption agreed" xml:space="preserve"> + <source>encryption agreed</source> + <target>salaus sovittu</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption agreed for %@" xml:space="preserve"> + <source>encryption agreed for %@</source> + <target>salaus sovittu %@:lle</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok" xml:space="preserve"> + <source>encryption ok</source> + <target>salaus ok</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok for %@" xml:space="preserve"> + <source>encryption ok for %@</source> + <target>salaus ok %@:lle</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> + <source>encryption re-negotiation allowed</source> + <target>salauksen uudelleenneuvottelu sallittu</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> + <source>encryption re-negotiation allowed for %@</source> + <target>salauksen uudelleenneuvottelu sallittu %@:lle</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required" xml:space="preserve"> + <source>encryption re-negotiation required</source> + <target>tarvitaan salauksen uudelleenneuvottelu</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> + <source>encryption re-negotiation required for %@</source> + <target>tarvitaan salauksen uudelleenneuvottelu %@:lle</target> + <note>chat item text</note> + </trans-unit> <trans-unit id="ended" xml:space="preserve"> <source>ended</source> + <target>päättyi</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="ended call %@" xml:space="preserve"> <source>ended call %@</source> + <target>puhelu päättyi %@:lle</target> <note>call status</note> </trans-unit> <trans-unit id="error" xml:space="preserve"> <source>error</source> + <target>virhe</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>tapahtuma tapahtui</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> + <target>ryhmä poistettu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="group profile updated" xml:space="preserve"> <source>group profile updated</source> + <target>ryhmäprofiili päivitetty</target> <note>snd group event chat item</note> </trans-unit> + <trans-unit id="hours" xml:space="preserve"> + <source>hours</source> + <target>tuntia</target> + <note>time unit</note> + </trans-unit> <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> + <target>iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen - se mahdollistaa push-ilmoitusten vastaanottamisen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> + <target>iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen sen muuttamisen tai sovelluksen uudelleen käynnistämisen jälkeen - se mahdollistaa push-ilmoitusten vastaanottamisen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> + <target>incognito kontaktilinkin kautta</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via group link" xml:space="preserve"> <source>incognito via group link</source> + <target>incognito ryhmälinkin kautta</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via one-time link" xml:space="preserve"> <source>incognito via one-time link</source> + <target>incognito kertalinkillä</target> <note>chat list item description</note> </trans-unit> <trans-unit id="indirect (%d)" xml:space="preserve"> <source>indirect (%d)</source> + <target>epäsuora (%d)</target> <note>connection level description</note> </trans-unit> <trans-unit id="invalid chat" xml:space="preserve"> <source>invalid chat</source> + <target>virheellinen keskustelu</target> <note>invalid chat data</note> </trans-unit> <trans-unit id="invalid chat data" xml:space="preserve"> <source>invalid chat data</source> + <target>virheelliset keskustelu-tiedot</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invalid data" xml:space="preserve"> <source>invalid data</source> + <target>virheelliset tiedot</target> <note>invalid chat item</note> </trans-unit> <trans-unit id="invitation to group %@" xml:space="preserve"> <source>invitation to group %@</source> + <target>kutsu ryhmään %@</target> <note>group name</note> </trans-unit> <trans-unit id="invited" xml:space="preserve"> <source>invited</source> + <target>kutsuttu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="invited %@" xml:space="preserve"> <source>invited %@</source> + <target>kutsuttu %@</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="invited to connect" xml:space="preserve"> <source>invited to connect</source> + <target>kutsuttu yhteydenpitoon</target> <note>chat list item title</note> </trans-unit> <trans-unit id="invited via your group link" xml:space="preserve"> <source>invited via your group link</source> + <target>kutsuttu ryhmäsi linkin kautta</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="italic" xml:space="preserve"> <source>italic</source> + <target>kursivoitu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="join as %@" xml:space="preserve"> <source>join as %@</source> + <target>Liity %@:nä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="left" xml:space="preserve"> <source>left</source> + <target>poistunut</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="marked deleted" xml:space="preserve"> <source>marked deleted</source> + <target>merkitty poistetuksi</target> <note>marked deleted chat item preview text</note> </trans-unit> <trans-unit id="member" xml:space="preserve"> <source>member</source> + <target>jäsen</target> <note>member role</note> </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> + <target>yhdistetty</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="message received" xml:space="preserve"> <source>message received</source> + <target>viesti vastaanotettu</target> <note>notification</note> </trans-unit> + <trans-unit id="minutes" xml:space="preserve"> + <source>minutes</source> + <target>minuuttia</target> + <note>time unit</note> + </trans-unit> <trans-unit id="missed call" xml:space="preserve"> <source>missed call</source> + <target>vastaamaton puhelu</target> <note>call status</note> </trans-unit> <trans-unit id="moderated" xml:space="preserve"> <source>moderated</source> + <target>moderoitu</target> <note>moderated chat item</note> </trans-unit> <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> + <target>%@ moderoi</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="months" xml:space="preserve"> + <source>months</source> + <target>kuukautta</target> + <note>time unit</note> + </trans-unit> <trans-unit id="never" xml:space="preserve"> <source>never</source> + <target>ei koskaan</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="new message" xml:space="preserve"> <source>new message</source> + <target>uusi viesti</target> <note>notification</note> </trans-unit> <trans-unit id="no" xml:space="preserve"> <source>no</source> + <target>ei</target> <note>pref value</note> </trans-unit> <trans-unit id="no e2e encryption" xml:space="preserve"> <source>no e2e encryption</source> + <target>ei e2e-salausta</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="no text" xml:space="preserve"> + <source>no text</source> + <target>ei tekstiä</target> + <note>copied message info in history</note> + </trans-unit> <trans-unit id="observer" xml:space="preserve"> <source>observer</source> + <target>tarkkailija</target> <note>member role</note> </trans-unit> <trans-unit id="off" xml:space="preserve"> <source>off</source> + <target>pois</target> <note>enabled status group pref value</note> </trans-unit> <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> + <target>tarjottu %@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="offered %@: %@" xml:space="preserve"> <source>offered %1$@: %2$@</source> + <target>tarjottu %1$@: %2$@</target> <note>feature offered item</note> </trans-unit> <trans-unit id="on" xml:space="preserve"> <source>on</source> + <target>päällä</target> <note>group pref value</note> </trans-unit> <trans-unit id="or chat with the developers" xml:space="preserve"> <source>or chat with the developers</source> + <target>tai keskustele kehittäjien kanssa</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="owner" xml:space="preserve"> <source>owner</source> + <target>omistaja</target> <note>member role</note> </trans-unit> <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> + <target>vertais</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> + <target>vastaus saatu…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="received confirmation…" xml:space="preserve"> <source>received confirmation…</source> + <target>vahvistus saatu…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> + <target>hylätty puhelu</target> <note>call status</note> </trans-unit> <trans-unit id="removed" xml:space="preserve"> <source>removed</source> + <target>poistettu</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="removed %@" xml:space="preserve"> <source>removed %@</source> + <target>%@ poistettu</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> + <target>poisti sinut</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="sec" xml:space="preserve"> <source>sec</source> + <target>sek</target> <note>network option</note> </trans-unit> + <trans-unit id="seconds" xml:space="preserve"> + <source>seconds</source> + <target>sekuntia</target> + <note>time unit</note> + </trans-unit> <trans-unit id="secret" xml:space="preserve"> <source>secret</source> + <target>salainen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="security code changed" xml:space="preserve"> + <source>security code changed</source> + <target>turvakoodi on muuttunut</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> + <target>alkaa…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="strike" xml:space="preserve"> <source>strike</source> + <target>soita</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="this contact" xml:space="preserve"> <source>this contact</source> + <target>tämä kontakti</target> <note>notification title</note> </trans-unit> <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> + <target>tuntematon</target> <note>connection info</note> </trans-unit> <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> + <target>päivitetty ryhmäprofiili</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="v%@ (%@)" xml:space="preserve"> <source>v%@ (%@)</source> + <target>v%@ (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="via contact address link" xml:space="preserve"> <source>via contact address link</source> + <target>kontaktiosoitelinkillä</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via group link" xml:space="preserve"> <source>via group link</source> + <target>ryhmälinkillä</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via one-time link" xml:space="preserve"> <source>via one-time link</source> + <target>kertalinkillä</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via relay" xml:space="preserve"> <source>via relay</source> + <target>releellä</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> + <target>videopuhelu (ei e2e-salattu)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="waiting for answer…" xml:space="preserve"> <source>waiting for answer…</source> + <target>odottaa vastaamista…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="waiting for confirmation…" xml:space="preserve"> <source>waiting for confirmation…</source> + <target>odottaa vahvistusta…</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="wants to connect to you!" xml:space="preserve"> <source>wants to connect to you!</source> + <target>haluaa olla yhteydessä sinuun!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="weeks" xml:space="preserve"> + <source>weeks</source> + <target>viikkoa</target> + <note>time unit</note> + </trans-unit> <trans-unit id="yes" xml:space="preserve"> <source>yes</source> + <target>kyllä</target> <note>pref value</note> </trans-unit> <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> + <target>sinut on kutsuttu ryhmään</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="you are observer" xml:space="preserve"> <source>you are observer</source> + <target>olet tarkkailija</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> + <target>muutit osoitetta</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed address for %@" xml:space="preserve"> <source>you changed address for %@</source> + <target>muutit osoitetta %@:ksi</target> <note>chat item text</note> </trans-unit> <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> <source>you changed role for yourself to %@</source> + <target>vaihdoit roolin itsellesi %@:ksi</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> <source>you changed role of %1$@ to %2$@</source> + <target>olet vaihtanut %1$@:n roolin %2$@:ksi</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you left" xml:space="preserve"> <source>you left</source> + <target>lähdit</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you removed %@" xml:space="preserve"> <source>you removed %@</source> + <target>poistit %@</target> <note>snd group event chat item</note> </trans-unit> <trans-unit id="you shared one-time link" xml:space="preserve"> <source>you shared one-time link</source> + <target>jaoit kertalinkin</target> <note>chat list item description</note> </trans-unit> <trans-unit id="you shared one-time link incognito" xml:space="preserve"> <source>you shared one-time link incognito</source> + <target>jaoit kertalinkin incognito-tilassa</target> <note>chat list item description</note> </trans-unit> <trans-unit id="you: " xml:space="preserve"> <source>you: </source> + <target>sinä: </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="~strike~" xml:space="preserve"> <source>\~strike~</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> - <source>%@ (current)</source> - <target state="translated">%@ (nykyinen)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> - <source>%@ (current):</source> - <target state="translated">% (nykyinen):</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve" approved="no"> - <source>%@ servers</source> - <target state="translated">%@ palvelimet</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> - <source>%lld minutes</source> - <target state="translated">%lld minuuttia</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@:" xml:space="preserve" approved="no"> - <source>%@:</source> - <target state="translated">%@:</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="%d weeks" xml:space="preserve" approved="no"> - <source>%d weeks</source> - <target state="translated">%d viikkoa</target> - <note>time interval</note> - </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> - <source>%lld seconds</source> - <target state="translated">%lld sekuntia</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="5 minutes" xml:space="preserve" approved="no"> - <source>5 minutes</source> - <target state="translated">5 minuuttia</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="30 seconds" xml:space="preserve" approved="no"> - <source>30 seconds</source> - <target state="translated">30 sekuntia</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> - <source>%u messages skipped.</source> - <target state="translated">%u viestit ohitettu.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> - <source>%u messages failed to decrypt.</source> - <target state="translated">%u viestien salauksen purku epäonnistui.</target> + <target>\~strike~</target> <note>No comment provided by engineer.</note> </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="fi" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX</source> + <target>SimpleX</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> + <target>SimpleX tarvitsee pääsyn kameraan, jotta se voi skannata QR-koodeja muodostaakseen yhteyden muihin käyttäjiin ja videopuheluita varten.</target> <note>Privacy - Camera Usage Description</note> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> + <target>SimpleX käyttää Face ID:tä paikalliseen todennukseen</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> + <target>SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten.</target> <note>Privacy - Microphone Usage Description</note> </trans-unit> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> + <target>SimpleX tarvitsee pääsyn valokuvakirjastoon kuvattujen ja vastaanotettujen medioiden tallentamista varten</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="fi" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle display name</note> </trans-unit> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> + <target>Copyright © 2022 SimpleX Chat. Kaikki oikeudet pidätetään.</target> <note>Copyright (human-readable)</note> </trans-unit> </body> diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..124ddbcc33 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cf485752ea --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,30 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..3af673b19f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,10 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json new file mode 100644 index 0000000000..0e3ae6dc56 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "fi", + "toolInfo" : { + "toolBuildNumber" : "15A240d", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "15.0" + }, + "version" : "1.0" +} \ No newline at end of file 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 6ae4c956c2..78f7fca921 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="fr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 coloré!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Historique</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## En réponse à</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#secret#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ et %@ sont connecté.es</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ à %2$@ :</target> @@ -102,6 +122,11 @@ <target>%@ veut se connecter !</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ et %lld autres membres sont connectés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@ :</target> @@ -172,6 +197,11 @@ <target>%lld minutes</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld nouvelles langues d'interface</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld seconde·s</target> @@ -302,6 +332,15 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- connexion au [service d'annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA) ! +- les accusés de réception (jusqu'à 20 membres). +- plus rapide et plus stable.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +436,9 @@ <target>Un nouveau contact</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Un profil aléatoire sera envoyé au contact qui vous a envoyé ce lien</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Un profil aléatoire sera envoyé à votre contact</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Un nouveau profil aléatoire sera partagé.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +494,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Accepter le contact</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Accepter la demande de connexion ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +507,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accepter en incognito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -680,6 +714,11 @@ <target>Build de l'app : %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>L'application chiffre les nouveaux fichiers locaux (sauf les vidéos).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Icône de l'app</target> @@ -757,7 +796,7 @@ </trans-unit> <trans-unit id="Auto-accept" xml:space="preserve"> <source>Auto-accept</source> - <target>Auto-acceptation</target> + <target>Auto-accepter</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Auto-accept contact requests" xml:space="preserve"> @@ -815,6 +854,11 @@ <target>Vous et votre contact êtes tous deux en mesure d'envoyer des messages vocaux.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -1046,9 +1090,19 @@ <target>Se connecter</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Se connecter via le lien du contact ?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Se connecter directement</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Se connecter incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Se connecter via un lien de contact</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1120,9 @@ <target>Se connecter via un lien / code QR</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Se connecter via un lien unique ?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Se connecter via un lien unique</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1150,6 @@ <target>Erreur de connexion (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Demande de connexion</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Demande de connexion envoyée !</target> @@ -1206,6 +1255,11 @@ <target>Créer un lien</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Créer un lien d'invitation unique</target> @@ -1268,7 +1322,7 @@ </trans-unit> <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> <source>Database IDs and Transport isolation option.</source> - <target>IDs de base de données et option d'isolation du transport.</target> + <target>IDs de base de données et option d'isolement du transport.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database downgrade" xml:space="preserve"> @@ -1549,6 +1603,11 @@ <target>Supprimé à : %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Distribution</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Les accusés de réception sont désactivés !</target> @@ -1591,7 +1650,7 @@ </trans-unit> <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> <source>Different names, avatars and transport isolation.</source> - <target>Différents noms, avatars et mode d'isolation de transport.</target> + <target>Différents noms, avatars et modes d'isolement de transport.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Direct messages" xml:space="preserve"> @@ -1654,6 +1713,11 @@ <target>Se déconnecter</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Découvrir et rejoindre des groupes</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Nom affiché</target> @@ -1789,6 +1853,16 @@ <target>Chiffrer la base de données ?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Chiffrer les fichiers locaux</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Chiffrement des fichiers et des médias stockés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Base de données chiffrée</target> @@ -1914,11 +1988,20 @@ <target>Erreur lors de la création du lien du groupe</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Erreur lors de la création du profil !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Erreur lors du déchiffrement du fichier</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Erreur lors de la suppression de la base de données du chat</target> @@ -2039,6 +2122,10 @@ <target>Erreur lors de l'envoi de l'e-mail</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Erreur lors de l'envoi du message</target> @@ -2437,7 +2524,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Historique</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2634,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>En réponse à</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2646,9 @@ <target>Mode Incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Le mode Incognito n'est pas supporté ici - votre profil principal sera envoyé aux membres du groupe</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2723,11 @@ <target>Adresse de serveur invalide !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Statut invalide</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Invitation expirée !</target> @@ -2900,7 +2987,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Erreur de distribution du message</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3074,11 @@ <target>Plus d'améliorations à venir !</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Connexion probablement supprimée.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Il est fort probable que ce contact ait supprimé la connexion avec vous.</target> @@ -3047,6 +3139,11 @@ <target>Nouvelle archive de base de données</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Nouvelle application de bureau !</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Nouveau nom d'affichage</target> @@ -3092,6 +3189,11 @@ <target>Aucun contact à ajouter</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Pas d'information sur la distribution</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Pas de token d'appareil !</target> @@ -3256,6 +3358,10 @@ <target>Seul votre contact peut envoyer des messages vocaux.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Ouvrir les Paramètres</target> @@ -3346,10 +3452,10 @@ <target>Coller le lien reçu</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3596,6 +3702,11 @@ <target>Pour en savoir plus, consultez notre [dépôt GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Les accusés de réception sont désactivés</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Reçu à</target> @@ -3666,8 +3777,8 @@ <target>Rejeter</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Rejeter le contact (l'expéditeur N'en est PAS informé)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3923,7 +4034,7 @@ </trans-unit> <trans-unit id="Search" xml:space="preserve"> <source>Search</source> - <target>Chercher</target> + <target>Recherche</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Secure queue" xml:space="preserve"> @@ -3986,6 +4097,10 @@ <target>Envoi de message direct</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Envoyer un message éphémère</target> @@ -4056,11 +4171,21 @@ <target>L'envoi d'accusés de réception est désactivé pour %lld contacts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>L'envoi de reçus est désactivé pour les groupes %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>L'envoi d'accusés de réception est activé pour %lld contacts</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>L'envoi de reçus est activé pour les groupes %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Envoi via</target> @@ -4201,6 +4326,11 @@ <target>Afficher les options pour les développeurs</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Voir les derniers messages</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Montrer l'aperçu</target> @@ -4271,6 +4401,11 @@ <target>Invitation unique SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Mode incognito simplifié</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Passer</target> @@ -4281,6 +4416,11 @@ <target>Messages manqués</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Petits groupes (max 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Des erreurs non fatales se sont produites lors de l'importation - vous pouvez consulter la console de chat pour plus de détails.</target> @@ -4408,7 +4548,7 @@ </trans-unit> <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> - <target>Appuyez pour activer le profil.</target> + <target>Appuyez pour activer un profil.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Tap to join" xml:space="preserve"> @@ -4573,9 +4713,9 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Ces paramètres s'appliquent à votre profil actuel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Ils peuvent être remplacés dans les paramètres des contacts</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Ils peuvent être modifiés dans les paramètres des contacts et des groupes.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4733,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Cette action ne peut être annulée - votre profil, vos contacts, vos messages et vos fichiers seront irréversiblement perdus.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Ce groupe compte plus de %lld membres, les accusés de réception ne sont pas envoyés.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Ce groupe n'existe plus.</target> @@ -4613,11 +4758,6 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. <target>Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l'application.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Pour trouver le profil utilisé lors d'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Pour établir une nouvelle connexion</target> @@ -4660,9 +4800,14 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <target>Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Basculer en mode incognito lors de la connexion.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> - <target>Isolement du transport</target> + <target>Transport isolé</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> @@ -4698,7 +4843,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Erreur inattendue : %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4799,7 +4944,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien </trans-unit> <trans-unit id="Update transport isolation mode?" xml:space="preserve"> <source>Update transport isolation mode?</source> - <target>Mettre à jour le mode d'isolation du transport ?</target> + <target>Mettre à jour le mode d'isolement du transport ?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> @@ -4837,6 +4982,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Utiliser le chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Utiliser le profil actuel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Utiliser pour les nouvelles connexions</target> @@ -4847,6 +4997,11 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Utiliser l'interface d'appel d'iOS</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Utiliser un nouveau profil incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Utiliser ce serveur</target> @@ -5137,8 +5292,8 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>Vous avez invité votre contact</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5267,11 +5422,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien <target>Votre profil de chat sera envoyé aux membres du groupe</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Votre profil de chat sera envoyé à votre contact</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Vos profils de chat</target> @@ -5326,6 +5476,11 @@ Vous pouvez modifier ce choix dans les Paramètres.</target> <target>Votre vie privée</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Votre profil **%@** sera partagé.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5333,11 +5488,6 @@ SimpleX servers cannot see your profile.</source> Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Votre profil sera envoyé au contact qui vous a envoyé ce lien</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil.</target> @@ -5405,7 +5555,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> - <target>acceptant le chiffrement pour %@…</target> + <target>accord sur le chiffrement pour %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="agreeing encryption…" xml:space="preserve"> @@ -5503,6 +5653,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>connecté</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>connexion</target> @@ -5613,6 +5767,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>direct</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>désactivé</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>message dupliqué</target> @@ -5650,7 +5809,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </trans-unit> <trans-unit id="encryption ok" xml:space="preserve"> <source>encryption ok</source> - <target>chiffrement ok</target> + <target>chiffrement OK</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok for %@" xml:space="preserve"> @@ -5693,6 +5852,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>erreur</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>event happened</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>groupe supprimé</target> @@ -5954,6 +6118,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> <target>code de sécurité modifié</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>lancement…</target> @@ -6098,7 +6266,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6130,7 +6298,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="fr" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json index c13b11b2ad..7df7c8ed26 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fr", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index 85f8b3a345..ac71ed26bc 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -8,7 +8,7 @@ <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> - <target state="needs-translation"> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> @@ -19,22 +19,22 @@ Available in v5.1</source> </trans-unit> <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> - <target state="needs-translation"> </target> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> - <target state="needs-translation"> </target> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" " xml:space="preserve" approved="no"> <source> </source> - <target state="needs-translation"> </target> + <target state="translated"> </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" (" xml:space="preserve" approved="no"> <source> (</source> - <target state="needs-translation"> (</target> + <target state="translated"> (</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> @@ -279,12 +279,12 @@ Available in v5.1</source> </trans-unit> <trans-unit id=", " xml:space="preserve" approved="no"> <source>, </source> - <target state="needs-translation">, </target> + <target state="translated">, </target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="." xml:space="preserve" approved="no"> <source>.</source> - <target state="needs-translation">.</target> + <target state="translated">.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="1 day" xml:space="preserve" approved="no"> @@ -1971,8 +1971,9 @@ Available in v5.1</source> <target state="translated">חברי הקבוצה יכולים לשלוח הודעות קוליות.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group message:" xml:space="preserve"> + <trans-unit id="Group message:" xml:space="preserve" approved="no"> <source>Group message:</source> + <target state="translated">הודעה קבוצתית:</target> <note>notification</note> </trans-unit> <trans-unit id="Group moderation" xml:space="preserve" approved="no"> @@ -2377,262 +2378,327 @@ Available in v5.1</source> <target state="translated">נתוני פרופיל מקומיים בלבד</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Lock after" xml:space="preserve"> + <trans-unit id="Lock after" xml:space="preserve" approved="no"> <source>Lock after</source> + <target state="translated">נעל אחרי</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Lock mode" xml:space="preserve"> + <trans-unit id="Lock mode" xml:space="preserve" approved="no"> <source>Lock mode</source> + <target state="translated">מצב נעילה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve"> + <trans-unit id="Make a private connection" xml:space="preserve" approved="no"> <source>Make a private connection</source> + <target state="translated">צור חיבור פרטי</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make profile private!" xml:space="preserve"> + <trans-unit id="Make profile private!" xml:space="preserve" approved="no"> <source>Make profile private!</source> + <target state="translated">הפוך את הפרופיל לפרטי!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> + <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve" approved="no"> <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> + <target state="translated">ודא שכתובות השרת %@ הן בפורמט הנכון, מופרדות בשורה ואינן משוכפלות (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> + <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve" approved="no"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> + <target state="translated">ודאו שכתובות שרתי ה־WebRTC ICE הן בפורמט הנכון, מופרדות בשורה ולא משוכפלות.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> + <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve" approved="no"> <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> + <target state="translated">אנשים רבים שאלו: *אם ל-SimpleX אין מזהי משתמש, איך הוא יכול לשלוח הודעות?*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mark deleted for everyone" xml:space="preserve"> + <trans-unit id="Mark deleted for everyone" xml:space="preserve" approved="no"> <source>Mark deleted for everyone</source> + <target state="translated">לסמן נמחק לכולם</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mark read" xml:space="preserve"> + <trans-unit id="Mark read" xml:space="preserve" approved="no"> <source>Mark read</source> + <target state="translated">סמן כנקרא</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mark verified" xml:space="preserve"> + <trans-unit id="Mark verified" xml:space="preserve" approved="no"> <source>Mark verified</source> + <target state="translated">סמן מאומת</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Markdown in messages" xml:space="preserve"> + <trans-unit id="Markdown in messages" xml:space="preserve" approved="no"> <source>Markdown in messages</source> + <target state="translated">מרקדאון בהודעות</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> + <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve" approved="no"> <source>Max 30 seconds, received instantly.</source> + <target state="translated">מקסימום 30 שניות, התקבל באופן מיידי.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member" xml:space="preserve"> + <trans-unit id="Member" xml:space="preserve" approved="no"> <source>Member</source> + <target state="translated">חבר קבוצה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> + <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve" approved="no"> <source>Member role will be changed to "%@". All group members will be notified.</source> + <target state="translated">תפקיד חבר הקבוצה ישתנה ל-"%@". כל חברי הקבוצה יקבלו הודעה.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> + <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve" approved="no"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> + <target state="translated">תפקיד חבר הקבוצה ישתנה ל-"%@". חבר הקבוצה יקבל הזמנה חדשה.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> + <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve" approved="no"> <source>Member will be removed from group - this cannot be undone!</source> + <target state="translated">חבר הקבוצה יוסר מהקבוצה – לא ניתן לבטל זאת!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message delivery error" xml:space="preserve"> + <trans-unit id="Message delivery error" xml:space="preserve" approved="no"> <source>Message delivery error</source> + <target state="translated">שגיאת מסירת הודעה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message draft" xml:space="preserve"> + <trans-unit id="Message draft" xml:space="preserve" approved="no"> <source>Message draft</source> + <target state="translated">טיוטת הודעה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message text" xml:space="preserve"> + <trans-unit id="Message text" xml:space="preserve" approved="no"> <source>Message text</source> + <target state="translated">טקסט הודעה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Messages" xml:space="preserve"> + <trans-unit id="Messages" xml:space="preserve" approved="no"> <source>Messages</source> + <target state="translated">הודעות</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Messages & files" xml:space="preserve"> + <trans-unit id="Messages & files" xml:space="preserve" approved="no"> <source>Messages & files</source> + <target state="translated">הודעות וקבצים</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Migrating database archive..." xml:space="preserve"> <source>Migrating database archive...</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migration error:" xml:space="preserve"> + <trans-unit id="Migration error:" xml:space="preserve" approved="no"> <source>Migration error:</source> + <target state="translated">שגיאת העברה:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> + <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve" approved="no"> <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> + <target state="translated">ההעברה נכשלה. הקש על **דלג** למטה כדי להמשיך להשתמש במסד הנתונים הנוכחי. אנא דווח על הבעיה למפתחי האפליקציה באמצעות צ'אט או דוא"ל [chat@simplex.chat](mailto:chat@simplex.chat).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migration is completed" xml:space="preserve"> + <trans-unit id="Migration is completed" xml:space="preserve" approved="no"> <source>Migration is completed</source> + <target state="translated">ההעברה הושלמה</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve"> + <trans-unit id="Migrations: %@" xml:space="preserve" approved="no"> <source>Migrations: %@</source> + <target state="translated">העברות: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Moderate" xml:space="preserve"> + <trans-unit id="Moderate" xml:space="preserve" approved="no"> <source>Moderate</source> + <target state="translated">חסימת הודעה</target> <note>chat item action</note> </trans-unit> - <trans-unit id="More improvements are coming soon!" xml:space="preserve"> + <trans-unit id="More improvements are coming soon!" xml:space="preserve" approved="no"> <source>More improvements are coming soon!</source> + <target state="translated">שיפורים נוספים יגיעו בקרוב!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> + <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve" approved="no"> <source>Most likely this contact has deleted the connection with you.</source> + <target state="translated">ככל הנראה איש קשר זה מחק את החיבור איתך.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Multiple chat profiles" xml:space="preserve"> + <trans-unit id="Multiple chat profiles" xml:space="preserve" approved="no"> <source>Multiple chat profiles</source> + <target state="translated">פרופילי צ׳אט מרובים</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mute" xml:space="preserve"> + <trans-unit id="Mute" xml:space="preserve" approved="no"> <source>Mute</source> + <target state="translated">השתק</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Muted when inactive!" xml:space="preserve"> + <trans-unit id="Muted when inactive!" xml:space="preserve" approved="no"> <source>Muted when inactive!</source> + <target state="translated">מושתק כאשר אין פעילות!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Name" xml:space="preserve"> + <trans-unit id="Name" xml:space="preserve" approved="no"> <source>Name</source> + <target state="translated">שם</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Network & servers" xml:space="preserve"> + <trans-unit id="Network & servers" xml:space="preserve" approved="no"> <source>Network & servers</source> + <target state="translated">רשת ושרתים</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Network settings" xml:space="preserve"> + <trans-unit id="Network settings" xml:space="preserve" approved="no"> <source>Network settings</source> + <target state="translated">הגדרות רשת</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Network status" xml:space="preserve"> + <trans-unit id="Network status" xml:space="preserve" approved="no"> <source>Network status</source> + <target state="translated">מצב רשת</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New Passcode" xml:space="preserve"> + <trans-unit id="New Passcode" xml:space="preserve" approved="no"> <source>New Passcode</source> + <target state="translated">קוד גישה חדש</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New contact request" xml:space="preserve"> + <trans-unit id="New contact request" xml:space="preserve" approved="no"> <source>New contact request</source> + <target state="translated">בקשה חדשה ליצירת קשר</target> <note>notification</note> </trans-unit> - <trans-unit id="New contact:" xml:space="preserve"> + <trans-unit id="New contact:" xml:space="preserve" approved="no"> <source>New contact:</source> + <target state="translated">איש קשר חדש:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve"> + <trans-unit id="New database archive" xml:space="preserve" approved="no"> <source>New database archive</source> + <target state="translated">ארכיון מסד נתונים חדש</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New in %@" xml:space="preserve"> + <trans-unit id="New in %@" xml:space="preserve" approved="no"> <source>New in %@</source> + <target state="translated">חדש ב %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New member role" xml:space="preserve"> + <trans-unit id="New member role" xml:space="preserve" approved="no"> <source>New member role</source> + <target state="translated">תפקיד חבר קבוצה חדש</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New message" xml:space="preserve"> + <trans-unit id="New message" xml:space="preserve" approved="no"> <source>New message</source> + <target state="translated">הודעה חדשה</target> <note>notification</note> </trans-unit> - <trans-unit id="New passphrase…" xml:space="preserve"> + <trans-unit id="New passphrase…" xml:space="preserve" approved="no"> <source>New passphrase…</source> + <target state="translated">סיסמה חדשה…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No" xml:space="preserve"> + <trans-unit id="No" xml:space="preserve" approved="no"> <source>No</source> + <target state="translated">לא</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No app password" xml:space="preserve"> + <trans-unit id="No app password" xml:space="preserve" approved="no"> <source>No app password</source> + <target state="translated">אין סיסמה לאפליקציה</target> <note>Authentication unavailable</note> </trans-unit> - <trans-unit id="No contacts selected" xml:space="preserve"> + <trans-unit id="No contacts selected" xml:space="preserve" approved="no"> <source>No contacts selected</source> + <target state="translated">לא נבחרו אנשי קשר</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No contacts to add" xml:space="preserve"> + <trans-unit id="No contacts to add" xml:space="preserve" approved="no"> <source>No contacts to add</source> + <target state="translated">אין אנשי קשר להוסיף</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No device token!" xml:space="preserve"> + <trans-unit id="No device token!" xml:space="preserve" approved="no"> <source>No device token!</source> + <target state="translated">אין אסימון מכשיר!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No group!" xml:space="preserve"> + <trans-unit id="No group!" xml:space="preserve" approved="no"> <source>Group not found!</source> + <target state="translated">קבוצה לא נמצאה!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No permission to record voice message" xml:space="preserve"> + <trans-unit id="No permission to record voice message" xml:space="preserve" approved="no"> <source>No permission to record voice message</source> + <target state="translated">אין הרשאה להקליט הודעה קולית</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No received or sent files" xml:space="preserve"> + <trans-unit id="No received or sent files" xml:space="preserve" approved="no"> <source>No received or sent files</source> + <target state="translated">לא התקבלו או נשלחו קבצים</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Notifications" xml:space="preserve"> + <trans-unit id="Notifications" xml:space="preserve" approved="no"> <source>Notifications</source> + <target state="translated">התראות</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Notifications are disabled!" xml:space="preserve"> + <trans-unit id="Notifications are disabled!" xml:space="preserve" approved="no"> <source>Notifications are disabled!</source> + <target state="translated">ההתראות מושבתות!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> + <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve" approved="no"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> + <target state="translated">כעת מנהלים יכולים: +- למחוק הודעות של חברי קבוצה. +- להשבית חברי קבוצה (תפקיד ”צופה”)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Off" xml:space="preserve"> + <trans-unit id="Off" xml:space="preserve" approved="no"> <source>Off</source> + <target state="translated">כבוי</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve"> + <trans-unit id="Off (Local)" xml:space="preserve" approved="no"> <source>Off (Local)</source> + <target state="translated">כבוי (מקומי)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Ok" xml:space="preserve"> + <trans-unit id="Ok" xml:space="preserve" approved="no"> <source>Ok</source> + <target state="translated">אישור</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database" xml:space="preserve"> + <trans-unit id="Old database" xml:space="preserve" approved="no"> <source>Old database</source> + <target state="translated">מסד נתונים ישן</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve"> + <trans-unit id="Old database archive" xml:space="preserve" approved="no"> <source>Old database archive</source> + <target state="translated">ארכיון מסד נתונים ישן</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="One-time invitation link" xml:space="preserve"> + <trans-unit id="One-time invitation link" xml:space="preserve" approved="no"> <source>One-time invitation link</source> + <target state="translated">קישור הזמנה חד־פעמי</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> + <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve" approved="no"> <source>Onion hosts will be required for connection. Requires enabling VPN.</source> + <target state="translated">לחיבור יידרשו מארחי Onion. דורש הפעלת VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> + <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve" approved="no"> <source>Onion hosts will be used when available. Requires enabling VPN.</source> + <target state="translated">מארחי Onion ישומשו כאשר יהיו זמינים. דורש הפעלת VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will not be used." xml:space="preserve"> + <trans-unit id="Onion hosts will not be used." xml:space="preserve" approved="no"> <source>Onion hosts will not be used.</source> + <target state="translated">לא ייעשה שימוש במארחי Onion.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> @@ -4976,6 +5042,275 @@ SimpleX servers cannot see your profile.</source> <target state="translated">אם תזינו קוד גישה זה בעת פתיחת האפליקציה, כל נתוני האפליקציה יימחקו באופן בלתי הפיך!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve" approved="no"> + <source>%1$@ at %2$@:</source> + <target state="translated">%1$@ בזמן %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="# %@" xml:space="preserve" approved="no"> + <source># %@</source> + <target state="translated"># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve" approved="no"> + <source>## History</source> + <target state="translated">## היסטוריה</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve" approved="no"> + <source>## In reply to</source> + <target state="translated">## בתגובה ל</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve" approved="no"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target state="translated">- שליחת הודעות יציבה יותר. +- קבוצות קצת יותר טובות. +- ועוד!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="A few more things" xml:space="preserve" approved="no"> + <source>A few more things</source> + <target state="translated">עוד כמה דברים</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve" approved="no"> + <source>A new random profile will be shared.</source> + <target state="translated">ישותף פרופיל אקראי חדש.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve" approved="no"> + <source>Accept connection request?</source> + <target state="translated">לאשר בקשת חיבור?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect directly" xml:space="preserve" approved="no"> + <source>Connect directly</source> + <target state="translated">התחבר ישירות</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve" approved="no"> + <source>Connect incognito</source> + <target state="translated">התחבר בזהות נסתרת</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via one-time link" xml:space="preserve" approved="no"> + <source>Connect via one-time link</source> + <target state="translated">התחבר באמצעות קישור חד־פעמי</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contacts" xml:space="preserve" approved="no"> + <source>Contacts</source> + <target state="translated">אנשי קשר</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery" xml:space="preserve" approved="no"> + <source>Delivery</source> + <target state="translated">מסירה</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error synchronizing connection" xml:space="preserve" approved="no"> + <source>Error synchronizing connection</source> + <target state="translated">שגיאה בסנכרון החיבור</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve" approved="no"> + <source>Fix encryption after restoring backups.</source> + <target state="translated">תקן הצפנה לאחר שחזור גיבויים.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by group member" xml:space="preserve" approved="no"> + <source>Fix not supported by group member</source> + <target state="translated">תיקון אינו נתמך על ידי חבר הקבוצה</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve" approved="no"> + <source>Invalid status</source> + <target state="translated">סטטוס לא חוקי</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Migrating database archive…" xml:space="preserve" approved="no"> + <source>Migrating database archive…</source> + <target state="translated">מעביר את ארכיון מסד הנתונים…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Moderated at: %@" xml:space="preserve" approved="no"> + <source>Moderated at: %@</source> + <target state="translated">נחסם ב: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve" approved="no"> + <source>Most likely this connection is deleted.</source> + <target state="translated">סביר להניח שהחיבור הזה נמחק.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve" approved="no"> + <source>%@ and %@ connected</source> + <target state="translated">%@ ו-%@ מחוברים</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve" approved="no"> + <source>Connect via contact link</source> + <target state="translated">התחבר באמצעות קישור איש קשר</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts!" xml:space="preserve" approved="no"> + <source>Delivery receipts!</source> + <target state="translated">קבלות על המשלוח!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve" approved="no"> + <source>Disable for all</source> + <target state="translated">השבת לכולם</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve" approved="no"> + <source>Error enabling delivery receipts!</source> + <target state="translated">שגיאה בהפעלת קבלות משלוח!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve" approved="no"> + <source>Even when disabled in the conversation.</source> + <target state="translated">גם אם הוא מושבת בשיחה.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix" xml:space="preserve" approved="no"> + <source>Fix</source> + <target state="translated">תקן</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection" xml:space="preserve" approved="no"> + <source>Fix connection</source> + <target state="translated">תקן את החיבור</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Find chats faster" xml:space="preserve" approved="no"> + <source>Find chats faster</source> + <target state="translated">מצא צ'אטים מהר יותר</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection?" xml:space="preserve" approved="no"> + <source>Fix connection?</source> + <target state="translated">לתקן את החיבור?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make one message disappear" xml:space="preserve" approved="no"> + <source>Make one message disappear</source> + <target state="translated">העלם הודעה אחת</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by contact" xml:space="preserve" approved="no"> + <source>Fix not supported by contact</source> + <target state="translated">תיקון לא נתמך על ידי איש קשר</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve" approved="no"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target state="translated">מצב זהות נסתרת מגן על הפרטיות שלך על ידי שימוש בפרופיל אקראי חדש עבור כל איש קשר.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In reply to" xml:space="preserve" approved="no"> + <source>In reply to</source> + <target state="translated">בתגובה ל</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep your connections" xml:space="preserve" approved="no"> + <source>Keep your connections</source> + <target state="translated">שימרו על הקשרים שלכם</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve" approved="no"> + <source>Message delivery receipts!</source> + <target state="translated">קבלות על הודעות!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve" approved="no"> + <source>Message reactions are prohibited in this chat.</source> + <target state="translated">תגובות אמוג׳י להודעות אסורות בצ׳אט זה.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no"> + <source>Message reactions are prohibited in this group.</source> + <target state="translated">תגובות אמוג׳י להודעות אסורות בקבוצה זו.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts are disabled!" xml:space="preserve" approved="no"> + <source>Delivery receipts are disabled!</source> + <target state="translated">קבלות על משלוח מושבתות!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve" approved="no"> + <source>Disable (keep overrides)</source> + <target state="translated">השבת (שמור עקיפות)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve" approved="no"> + <source>Don't enable</source> + <target state="translated">אל תפעיל</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve" approved="no"> + <source>Enable (keep overrides)</source> + <target state="translated">הפעל (שמור עקיפות)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve" approved="no"> + <source>Enable for all</source> + <target state="translated">הפעל עבור כולם</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve" approved="no"> + <source>Error setting delivery receipts!</source> + <target state="translated">שגיאה בהגדרת קבלות משלוח!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve" approved="no"> + <source>Filter unread and favorite chats.</source> + <target state="translated">סנן צ'אטים שלא נקראו וצ'אטים מועדפים.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Moderated at" xml:space="preserve" approved="no"> + <source>Moderated at</source> + <target state="translated">נחסם</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions" xml:space="preserve" approved="no"> + <source>Message reactions</source> + <target state="translated">תגובות אמוג׳י להודעות</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Exporting database archive…" xml:space="preserve" approved="no"> + <source>Exporting database archive…</source> + <target state="translated">מייצא את ארכיון מסד הנתונים…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve" approved="no"> + <source>%@, %@ and %lld other members connected</source> + <target state="translated">%@, %@ ו-%lld חברים אחרים מחוברים</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New display name" xml:space="preserve" approved="no"> + <source>New display name</source> + <target state="translated">שם תצוגה חדש</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve" approved="no"> + <source>No delivery information</source> + <target state="translated">אין מידע על מסירה</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No filtered chats" xml:space="preserve" approved="no"> + <source>No filtered chats</source> + <target state="translated">אין צ'אטים מסוננים</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No history" xml:space="preserve" approved="no"> + <source>No history</source> + <target state="translated">ללא היסטוריה</target> + <note>No comment provided by engineer.</note> + </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="he" datatype="plaintext"> 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 04163b80da..2e9b9a3264 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="it" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 colorato!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Cronologia</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## In risposta a</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#segreto#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ e %@ sono connessi/e</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ alle %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ si vuole connettere!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ e altri %lld membri sono connessi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,11 @@ <target>%lld minuti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld nuove lingue dell'interfaccia</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld secondo/i</target> @@ -302,6 +332,15 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- connessione al [servizio directory](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- ricevute di consegna (fino a 20 membri). +- più veloce e più stabile.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +436,9 @@ <target>Un contatto nuovo</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Verrà inviato un profilo casuale al contatto da cui hai ricevuto questo link</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Verrà inviato un profilo casuale al tuo contatto</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Verrà condiviso un nuovo profilo casuale.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +494,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Accetta il contatto</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Accettare la richiesta di connessione?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +507,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accetta in incognito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -680,6 +714,11 @@ <target>Build dell'app: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>L'app cripta i nuovi file locali (eccetto i video).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Icona app</target> @@ -815,6 +854,11 @@ <target>Sia tu che il tuo contatto potete inviare messaggi vocali.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -1046,9 +1090,19 @@ <target>Connetti</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Connettere via link del contatto?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Connetti direttamente</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Connetti in incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Connetti via link del contatto</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1120,9 @@ <target>Connetti via link / codice QR</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Connettere via link una tantum?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Connetti via link una tantum</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1150,6 @@ <target>Errore di connessione (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Richiesta di connessione</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Richiesta di connessione inviata!</target> @@ -1206,6 +1255,11 @@ <target>Crea link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Crea un nuovo profilo nell'[app desktop](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Crea link di invito una tantum</target> @@ -1549,6 +1603,11 @@ <target>Eliminato il: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Consegna</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Le ricevute di consegna sono disattivate!</target> @@ -1654,6 +1713,11 @@ <target>Disconnetti</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Scopri ed unisciti ai gruppi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Nome da mostrare</target> @@ -1789,6 +1853,16 @@ <target>Crittografare il database?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Cripta i file locali</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Crittografia di file e media memorizzati</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Database crittografato</target> @@ -1914,11 +1988,20 @@ <target>Errore nella creazione del link del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Errore nella creazione del profilo!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Errore decifrando il file</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Errore nell'eliminazione del database della chat</target> @@ -2039,6 +2122,10 @@ <target>Errore nell'invio dell'email</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Errore nell'invio del messaggio</target> @@ -2437,7 +2524,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Cronologia</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2634,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>In risposta a</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2646,9 @@ <target>Modalità incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>La modalità in incognito non è supportata qui: il tuo profilo principale verrà inviato ai membri del gruppo</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>La modalità in incognito protegge la privacy del nome e dell'immagine del tuo profilo principale: per ogni nuovo contatto viene creato un nuovo profilo casuale.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>La modalità in incognito protegge la tua privacy usando un nuovo profilo casuale per ogni contatto.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2723,11 @@ <target>Indirizzo del server non valido!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Stato non valido</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Invito scaduto!</target> @@ -2900,7 +2987,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Errore di recapito del messaggio</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3074,11 @@ <target>Altri miglioramenti sono in arrivo!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Probabilmente questa connessione è stata eliminata.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Probabilmente questo contatto ha eliminato la connessione con te.</target> @@ -3047,6 +3139,11 @@ <target>Nuovo archivio database</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Nuova app desktop!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Nuovo nome da mostrare</target> @@ -3092,6 +3189,11 @@ <target>Nessun contatto da aggiungere</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Nessuna informazione sulla consegna</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Nessun token del dispositivo!</target> @@ -3256,6 +3358,10 @@ <target>Solo il tuo contatto può inviare messaggi vocali.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Apri le impostazioni</target> @@ -3346,10 +3452,10 @@ <target>Incolla il link ricevuto</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3596,6 +3702,11 @@ <target>Maggiori informazioni nel nostro [repository GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Le ricevute sono disattivate</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Ricevuto il</target> @@ -3666,8 +3777,8 @@ <target>Rifiuta</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Rifiuta contatto (mittente NON avvisato)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3986,6 +4097,10 @@ <target>Invia messaggio diretto</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Invia messaggio a tempo</target> @@ -4056,11 +4171,21 @@ <target>L'invio di ricevute è disattivato per %lld contatti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>L'invio di ricevute è disattivato per %lld gruppi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>L'invio di ricevute è attivo per %lld contatti</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>L'invio di ricevute è attivo per %lld gruppi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Invio tramite</target> @@ -4201,6 +4326,11 @@ <target>Mostra opzioni sviluppatore</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Mostra ultimi messaggi</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Mostra anteprima</target> @@ -4271,6 +4401,11 @@ <target>Invito SimpleX una tantum</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Modalità incognito semplificata</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Salta</target> @@ -4281,6 +4416,11 @@ <target>Messaggi saltati</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Piccoli gruppi (max 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Si sono verificati alcuni errori non gravi durante l'importazione: vedi la console della chat per i dettagli.</target> @@ -4573,9 +4713,9 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Queste impostazioni sono per il tuo profilo attuale **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Possono essere sovrascritte nelle impostazioni dei contatti</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Possono essere sovrascritte nelle impostazioni dei contatti e dei gruppi.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4733,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Questa azione non può essere annullata: il tuo profilo, i contatti, i messaggi e i file andranno persi in modo irreversibile.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Questo gruppo ha più di %lld membri, le ricevute di consegna non vengono inviate.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Questo gruppo non esiste più.</target> @@ -4613,11 +4758,6 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.</ta <target>Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell'app.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Per trovare il profilo usato per una connessione in incognito, tocca il nome del contatto o del gruppo in cima alla chat.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Per creare una nuova connessione</target> @@ -4660,6 +4800,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <target>Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Attiva/disattiva l'incognito quando ti colleghi.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Isolamento del trasporto</target> @@ -4698,7 +4843,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Errore imprevisto: % @</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4837,6 +4982,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Usa la chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Usa il profilo attuale</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Usa per connessioni nuove</target> @@ -4847,6 +4997,11 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Usa interfaccia di chiamata iOS</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Usa nuovo profilo in incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Usa il server</target> @@ -5137,8 +5292,8 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Devi inserire la password ogni volta che si avvia l'app: non viene memorizzata sul dispositivo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>Hai invitato il contatto</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5267,11 +5422,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e <target>Il tuo profilo di chat verrà inviato ai membri del gruppo</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Il tuo profilo di chat verrà inviato al tuo contatto</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>I tuoi profili di chat</target> @@ -5326,6 +5476,11 @@ Puoi modificarlo nelle impostazioni.</target> <target>La tua privacy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Il tuo profilo **%@** verrà condiviso.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5333,11 +5488,6 @@ SimpleX servers cannot see your profile.</source> I server di SimpleX non possono vedere il tuo profilo.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Il tuo profilo verrà inviato al contatto da cui hai ricevuto questo link</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo.</target> @@ -5503,6 +5653,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>connesso/a</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>in connessione</target> @@ -5613,6 +5767,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>diretta</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>disattivato</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>messaggio duplicato</target> @@ -5693,6 +5852,11 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>errore</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>evento accaduto</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>gruppo eliminato</target> @@ -5805,7 +5969,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </trans-unit> <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> - <target>connesso/a</target> + <target>è connesso/a</target> <note>rcv group event chat item</note> </trans-unit> <trans-unit id="message received" xml:space="preserve"> @@ -5954,6 +6118,10 @@ I server di SimpleX non possono vedere il tuo profilo.</target> <target>codice di sicurezza modificato</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>avvio…</target> @@ -6098,7 +6266,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6130,7 +6298,7 @@ I server di SimpleX non possono vedere il tuo profilo.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="it" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/it.xcloc/contents.json b/apps/ios/SimpleX Localizations/it.xcloc/contents.json index 593ffad179..2ad653d36f 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/it.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "it", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 7f216c950e..27d7cb54f6 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="ja" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 色付き!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## 履歴</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## 返信先</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>シークレット</target> @@ -72,8 +87,14 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ と %@ は接続中</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> + <target>%1$@ at %2$@:</target> <note>copied message info, <sender> at <time></note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -101,6 +122,11 @@ <target>%@ が接続を希望しています!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ および %lld 人のメンバーが接続中</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -171,6 +197,10 @@ <target>%lld 分</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld 秒</target> @@ -301,10 +331,19 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. - and more!</source> + <target>- より安定したメッセージ配信。 +- 改良されたグループ。 +- などなど!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -385,6 +424,7 @@ </trans-unit> <trans-unit id="A few more things" xml:space="preserve"> <source>A few more things</source> + <target>その他</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A new contact" xml:space="preserve"> @@ -392,14 +432,9 @@ <target>新しい連絡先</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>このリンクの送信元にランダムなプロフィール(ダミー)が送られます</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>連絡先にランダムなプロフィール(ダミー)が送られます</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>新しいランダムなプロファイルが共有されます。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -416,14 +451,17 @@ </trans-unit> <trans-unit id="Abort" xml:space="preserve"> <source>Abort</source> + <target>中止</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address" xml:space="preserve"> <source>Abort changing address</source> + <target>アドレス変更の中止</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Abort changing address?" xml:space="preserve"> <source>Abort changing address?</source> + <target>アドレス変更を中止しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="About SimpleX" xml:space="preserve"> @@ -452,8 +490,8 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> <target>連絡を受け入れる</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -465,7 +503,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>シークレットモードで承諾</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -509,6 +547,7 @@ </trans-unit> <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> <source>Address change will be aborted. Old receiving address will be used.</source> + <target>アドレス変更は中止されます。古い受信アドレスが使用されます。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> @@ -603,6 +642,7 @@ </trans-unit> <trans-unit id="Allow to send files and media." xml:space="preserve"> <source>Allow to send files and media.</source> + <target>ファイルやメディアの送信を許可する。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow to send voice messages." xml:space="preserve"> @@ -670,6 +710,10 @@ <target>アプリのビルド: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>アプリのアイコン</target> @@ -777,6 +821,7 @@ </trans-unit> <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> + <target>より良いメッセージ</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> @@ -804,6 +849,10 @@ <target>あなたと連絡相手が音声メッセージを送信できます。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>チャット プロファイル経由 (デフォルト) または [接続経由](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -1035,8 +1084,18 @@ <target>接続</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>直接接続する</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>シークレットモードで接続</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> <target>連絡先リンク経由で接続しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1055,8 +1114,8 @@ <target>リンク・QRコード経由で接続</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> <target>使い捨てリンク経由で接続しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1085,11 +1144,6 @@ <target>接続エラー (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>接続のリクエスト</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>接続リクエストを送信しました!</target> @@ -1142,6 +1196,7 @@ </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> + <target>連絡先</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> @@ -1194,6 +1249,10 @@ <target>リンクを生成する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>使い捨ての招待リンクを生成する</target> @@ -1537,8 +1596,14 @@ <target>削除完了: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Delivery</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> + <target>Delivery receipts are disabled!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> @@ -1592,6 +1657,7 @@ </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> <source>Disable (keep overrides)</source> + <target>無効にする(設定の優先を維持)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1601,6 +1667,7 @@ </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> + <target>すべて無効</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> @@ -1638,6 +1705,10 @@ <target>切断</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>表示名</target> @@ -1665,6 +1736,7 @@ </trans-unit> <trans-unit id="Don't enable" xml:space="preserve"> <source>Don't enable</source> + <target>有効にしない</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> @@ -1709,6 +1781,7 @@ </trans-unit> <trans-unit id="Enable (keep overrides)" xml:space="preserve"> <source>Enable (keep overrides)</source> + <target>有効にする(設定の優先を維持)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> @@ -1728,6 +1801,7 @@ </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> <source>Enable for all</source> + <target>すべて有効</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> @@ -1770,6 +1844,15 @@ <target>データベースを暗号化しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>ローカルファイルを暗号化する</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>暗号化済みデータベース</target> @@ -1847,6 +1930,7 @@ </trans-unit> <trans-unit id="Error aborting address change" xml:space="preserve"> <source>Error aborting address change</source> + <target>アドレス変更中止エラー</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error accepting contact request" xml:space="preserve"> @@ -1894,11 +1978,20 @@ <target>グループリンク生成にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>プロフィール作成にエラー発生!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>ファイルの復号エラー</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>チャットデータベース削除にエラー発生</target> @@ -2018,6 +2111,10 @@ <target>メールの送信にエラー発生</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>メッセージ送信にエラー発生</target> @@ -2044,6 +2141,7 @@ </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> + <target>接続の同期エラー</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> @@ -2088,6 +2186,7 @@ </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> + <target>会話中に無効になっている場合でも。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exit without saving" xml:space="preserve"> @@ -2127,6 +2226,7 @@ </trans-unit> <trans-unit id="Favorite" xml:space="preserve"> <source>Favorite</source> + <target>お気に入り</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be deleted from servers." xml:space="preserve"> @@ -2156,50 +2256,62 @@ </trans-unit> <trans-unit id="Files and media" xml:space="preserve"> <source>Files and media</source> + <target>ファイルとメディア</target> <note>chat feature</note> </trans-unit> <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> <source>Files and media are prohibited in this group.</source> + <target>このグループでは、ファイルとメディアは禁止されています。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Files and media prohibited!" xml:space="preserve"> <source>Files and media prohibited!</source> + <target>ファイルとメディアは禁止されています!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> <source>Filter unread and favorite chats.</source> + <target>未読とお気に入りをフィルターします。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> + <target>ついに、私たちはそれらを手に入れました! 🚀</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Find chats faster" xml:space="preserve"> <source>Find chats faster</source> + <target>チャットを素早く検索</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> <source>Fix</source> + <target>修正</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection" xml:space="preserve"> <source>Fix connection</source> + <target>接続を修正</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection?" xml:space="preserve"> <source>Fix connection?</source> + <target>接続を修正しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> <source>Fix encryption after restoring backups.</source> + <target>バックアップの復元後に暗号化を修正します。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by contact" xml:space="preserve"> <source>Fix not supported by contact</source> + <target>連絡先による修正はサポートされていません</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by group member" xml:space="preserve"> <source>Fix not supported by group member</source> + <target>グループメンバーによる修正はサポートされていません</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="For console" xml:space="preserve"> @@ -2309,6 +2421,7 @@ </trans-unit> <trans-unit id="Group members can send files and media." xml:space="preserve"> <source>Group members can send files and media.</source> + <target>グループメンバーはファイルやメディアを送信できます。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Group members can send voice messages." xml:space="preserve"> @@ -2399,7 +2512,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>履歴</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2508,7 +2621,8 @@ </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> - <note>copied message info</note> + <target>返信先</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2520,14 +2634,9 @@ <target>シークレットモード</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>ここではシークレットモードが無効です。メインのプロフィールがグループのメンバーに送られます</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されます。</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2602,6 +2711,11 @@ <target>無効なサーバアドレス!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>無効なステータス</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>招待が期限切れました!</target> @@ -2695,6 +2809,7 @@ </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> + <target>接続を維持</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="KeyChain error" xml:space="preserve"> @@ -2789,6 +2904,7 @@ </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> + <target>メッセージを1つ消す</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make profile private!" xml:space="preserve"> @@ -2859,7 +2975,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>メッセージ送信エラー</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2945,6 +3061,11 @@ <target>まだまだ改善してまいります!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>おそらく、この接続は削除されています。</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>恐らくこの連絡先があなたとの接続を削除しました。</target> @@ -3005,6 +3126,10 @@ <target>新しいデータベースのアーカイブ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>新たな表示名</target> @@ -3050,6 +3175,11 @@ <target>追加できる連絡先がありません</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>送信情報なし</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>デバイストークンがありません!</target> @@ -3057,6 +3187,7 @@ </trans-unit> <trans-unit id="No filtered chats" xml:space="preserve"> <source>No filtered chats</source> + <target>フィルタされたチャットはありません</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No group!" xml:space="preserve"> @@ -3066,6 +3197,7 @@ </trans-unit> <trans-unit id="No history" xml:space="preserve"> <source>No history</source> + <target>履歴はありません</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> @@ -3154,6 +3286,7 @@ </trans-unit> <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> <source>Only group owners can enable files and media.</source> + <target>ファイルやメディアを有効にできるのは、グループオーナーだけです。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> @@ -3211,6 +3344,10 @@ <target>音声メッセージを送れるのはあなたの連絡相手だけです。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>設定を開く</target> @@ -3301,10 +3438,10 @@ <target>頂いたリンクを貼り付ける</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3478,6 +3615,7 @@ </trans-unit> <trans-unit id="Prohibit sending files and media." xml:space="preserve"> <source>Prohibit sending files and media.</source> + <target>ファイルやメディアの送信を禁止します。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> @@ -3502,6 +3640,7 @@ </trans-unit> <trans-unit id="Protocol timeout per KB" xml:space="preserve"> <source>Protocol timeout per KB</source> + <target>KB あたりのプロトコル タイムアウト</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> @@ -3516,6 +3655,7 @@ </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> + <target>反応する…</target> <note>chat item menu</note> </trans-unit> <trans-unit id="Read" xml:space="preserve"> @@ -3548,6 +3688,10 @@ <target>詳しくは[GitHubリポジトリ](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)をご覧ください。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>受信</target> @@ -3570,6 +3714,7 @@ </trans-unit> <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Receiving file will be stopped." xml:space="preserve"> @@ -3589,10 +3734,12 @@ </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>接続されているすべてのサーバーを再接続して、メッセージを強制的に配信します。 追加のトラフィックを使用します。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> + <target>サーバーに再接続しますか?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> @@ -3615,8 +3762,8 @@ <target>拒否</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>連絡を拒否(送信者には通知されません)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3657,14 +3804,17 @@ </trans-unit> <trans-unit id="Renegotiate" xml:space="preserve"> <source>Renegotiate</source> + <target>再ネゴシエート</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption" xml:space="preserve"> <source>Renegotiate encryption</source> + <target>暗号化の再ネゴシエート</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption?" xml:space="preserve"> <source>Renegotiate encryption?</source> + <target>暗号化を再ネゴシエートしますか?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> @@ -3931,6 +4081,10 @@ <target>ダイレクトメッセージを送信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>消えるメッセージを送信</target> @@ -3997,10 +4151,18 @@ <source>Sending receipts is disabled for %lld contacts</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>経由で送信</target> @@ -4141,6 +4303,11 @@ <target>開発者向けオプションを表示</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>最新のメッセージを表示</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>プレビューを表示</target> @@ -4211,6 +4378,10 @@ <target>SimpleX使い捨て招待リンク</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>スキップ</target> @@ -4221,8 +4392,14 @@ <target>飛ばしたメッセージ</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>小グループ(最大20名)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Somebody" xml:space="preserve"> @@ -4439,6 +4616,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> @@ -4478,6 +4656,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> <source>The second tick we missed! ✅</source> + <target>長らくお待たせしました! ✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -4507,10 +4686,12 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> <source>These settings are for your current profile **%@**.</source> + <target>これらの設定は現在のプロファイル **%@** 用です。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>これらは連絡先の設定が優先します。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4528,6 +4709,10 @@ It can happen because of some bug or when the connection is compromised.</source <target>あなたのプロフィール、連絡先、メッセージ、ファイルが完全削除されます (※元に戻せません※)。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>このグループはもう存在しません。</target> @@ -4548,11 +4733,6 @@ It can happen because of some bug or when the connection is compromised.</source <target>接続するにはQRコードを読み込むか、アプリ内のリンクを使用する必要があります。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>シークレットモード接続のプロフィールを確認するには、チャットの上部の連絡先、またはグループ名をタップします。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>新規に接続する場合</target> @@ -4595,6 +4775,10 @@ You will be prompted to complete authentication before this feature is enabled.< <target>エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>トランスポート隔離</target> @@ -4633,7 +4817,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>予期しないエラー: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4642,6 +4826,7 @@ You will be prompted to complete authentication before this feature is enabled.< </trans-unit> <trans-unit id="Unfav." xml:space="preserve"> <source>Unfav.</source> + <target>お気に入りを取り消す。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unhide" xml:space="preserve"> @@ -4771,6 +4956,11 @@ To connect, please ask your contact to create another connection link and check <target>チャット</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>現在のプロファイルを使用する</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>新しい接続に使う</target> @@ -4781,6 +4971,11 @@ To connect, please ask your contact to create another connection link and check <target>iOS通話インターフェースを使用する</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>新しいシークレットプロファイルを使用する</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>サーバを使う</target> @@ -4993,10 +5188,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="You can enable later via Settings" xml:space="preserve"> <source>You can enable later via Settings</source> + <target>あとで設定から有効にできます</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> <source>You can enable them later via app Privacy & Security settings.</source> + <target>あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> @@ -5069,8 +5266,8 @@ To connect, please ask your contact to create another connection link and check <target>アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>連絡先に招待を送りました</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5199,11 +5396,6 @@ To connect, please ask your contact to create another connection link and check <target>あなたのチャットプロフィールが他のグループメンバーに送られます</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>あなたのチャットプロフィールが連絡相手に送られます</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>あなたのチャットプロフィール</target> @@ -5258,6 +5450,11 @@ You can change it in Settings.</source> <target>あなたのプライバシー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>あなたのプロファイル **%@** が共有されます。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5265,11 +5462,6 @@ SimpleX servers cannot see your profile.</source> SimpleX サーバーはあなたのプロファイルを参照できません。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>あなたのプロフィールは、このリンクを受け取った連絡先に送信されます</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。</target> @@ -5337,10 +5529,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> + <target>%@の暗号化に同意しています…</target> <note>chat item text</note> </trans-unit> <trans-unit id="agreeing encryption…" xml:space="preserve"> <source>agreeing encryption…</source> + <target>暗号化に同意しています…</target> <note>chat item text</note> </trans-unit> <trans-unit id="always" xml:space="preserve"> @@ -5405,10 +5599,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> <source>changing address for %@…</source> + <target>%@ のアドレスを変更しています…</target> <note>chat item text</note> </trans-unit> <trans-unit id="changing address…" xml:space="preserve"> <source>changing address…</source> + <target>アドレスを変更しています…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> @@ -5431,6 +5627,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>接続中</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>接続待ち</target> @@ -5513,10 +5713,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> + <target>デフォルト(いいえ)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="default (yes)" xml:space="preserve"> <source>default (yes)</source> + <target>デフォルト(はい)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="deleted" xml:space="preserve"> @@ -5539,6 +5741,11 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>直接</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>無効</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>重複メッセージ</target> @@ -5566,34 +5773,42 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="encryption agreed" xml:space="preserve"> <source>encryption agreed</source> + <target>暗号化に同意しました</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption agreed for %@" xml:space="preserve"> <source>encryption agreed for %@</source> + <target>%@ の暗号化に同意しました</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok" xml:space="preserve"> <source>encryption ok</source> + <target>暗号化OK</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok for %@" xml:space="preserve"> <source>encryption ok for %@</source> + <target>%@ の暗号化OK</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> <source>encryption re-negotiation allowed</source> + <target>暗号化の再ネゴシエーションを許可</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> <source>encryption re-negotiation allowed for %@</source> + <target>%@ の暗号化の再ネゴシエーションを許可</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required" xml:space="preserve"> <source>encryption re-negotiation required</source> + <target>暗号化の再ネゴシエーションが必要</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> <source>encryption re-negotiation required for %@</source> + <target>%@ の暗号化の再ネゴシエーションが必要</target> <note>chat item text</note> </trans-unit> <trans-unit id="ended" xml:space="preserve"> @@ -5611,6 +5826,11 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 <target>エラー</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>イベント発生</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>グループ削除済み</target> @@ -5778,6 +5998,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="no text" xml:space="preserve"> <source>no text</source> + <target>テキストなし</target> <note>copied message info in history</note> </trans-unit> <trans-unit id="observer" xml:space="preserve"> @@ -5868,8 +6089,13 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </trans-unit> <trans-unit id="security code changed" xml:space="preserve"> <source>security code changed</source> + <target>セキュリティコードが変更されました</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>接続中…</target> @@ -6014,7 +6240,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6046,7 +6272,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ja" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json index 64b4ac74b6..7d3c224e68 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ja", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 d4c73f56c0..233b1d0ba1 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="nl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 gekleurd!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Geschiedenis</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## Als antwoord op</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#geheim#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ en %@ verbonden</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ bij %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ wil verbinding maken!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ en %lld andere leden hebben verbinding gemaakt</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,11 @@ <target>%lld minuten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld nieuwe interface-talen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld seconde(n)</target> @@ -219,7 +249,7 @@ </trans-unit> <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> <source>%u messages failed to decrypt.</source> - <target>%u-berichten kunnen niet worden gedecodeerd.</target> + <target>%u berichten kunnen niet worden ontsleuteld.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%u messages skipped." xml:space="preserve"> @@ -244,7 +274,7 @@ </trans-unit> <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> - <target>**Maak een link / QR-code aan** die uw contactpersoon kan gebruiken.</target> + <target>**Maak een link / QR-code aan** die uw contact kan gebruiken.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> @@ -274,7 +304,7 @@ </trans-unit> <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target>**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contactpersoon.</target> + <target>**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contact.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> @@ -302,6 +332,15 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- ontvangst bevestiging(tot 20 leden). +- sneller en stabieler.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +436,9 @@ <target>Een nieuw contact</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Er wordt een willekeurig profiel verzonden naar het contact van wie je deze link hebt ontvangen</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Er wordt een willekeurig profiel naar uw contactpersoon verzonden</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Een nieuw willekeurig profiel wordt gedeeld.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +494,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Accepteer contactpersoon</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Accepteer contact</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +507,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Accepteer incognito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -572,22 +606,22 @@ </trans-unit> <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> <source>Allow calls only if your contact allows them.</source> - <target>Sta oproepen alleen toe als uw contact persoon dit toestaat.</target> + <target>Sta oproepen alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> - <target>Sta verdwijnende berichten alleen toe als uw contactpersoon dit toestaat.</target> + <target>Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target>Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contactpersoon dit toestaat.</target> + <target>Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> <source>Allow message reactions only if your contact allows them.</source> - <target>Sta berichtreacties alleen toe als uw contactpersoon dit toestaat.</target> + <target>Sta berichtreacties alleen toe als uw contact dit toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow message reactions." xml:space="preserve"> @@ -622,7 +656,7 @@ </trans-unit> <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> <source>Allow voice messages only if your contact allows them.</source> - <target>Sta spraak berichten alleen toe als uw contactpersoon ze toestaat.</target> + <target>Sta spraak berichten alleen toe als uw contact ze toestaat.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow voice messages?" xml:space="preserve"> @@ -680,6 +714,11 @@ <target>App build: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>App versleutelt nieuwe lokale bestanden (behalve video's).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>App icon</target> @@ -777,7 +816,7 @@ </trans-unit> <trans-unit id="Bad message ID" xml:space="preserve"> <source>Bad message ID</source> - <target>Onjuiste bericht ID</target> + <target>Onjuiste bericht-ID</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Bad message hash" xml:space="preserve"> @@ -792,27 +831,32 @@ </trans-unit> <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> - <target>Zowel u als uw contactpersoon kunnen berichtreacties toevoegen.</target> + <target>Zowel u als uw contact kunnen berichtreacties toevoegen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> <source>Both you and your contact can irreversibly delete sent messages.</source> - <target>Zowel jij als je contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</target> + <target>Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> <source>Both you and your contact can make calls.</source> - <target>Zowel u als uw contact persoon kunnen bellen.</target> + <target>Zowel u als uw contact kunnen bellen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> <source>Both you and your contact can send disappearing messages.</source> - <target>Zowel jij als je contactpersoon kunnen verdwijnende berichten sturen.</target> + <target>Zowel jij als je contact kunnen verdwijnende berichten sturen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> <source>Both you and your contact can send voice messages.</source> - <target>Zowel jij als je contactpersoon kunnen spraak berichten verzenden.</target> + <target>Zowel jij als je contact kunnen spraak berichten verzenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> @@ -1046,8 +1090,18 @@ <target>Verbind</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Verbind direct</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Verbind incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> <target>Verbinden via contact link?</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1066,8 +1120,8 @@ <target>Maak verbinding via link / QR-code</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> <target>Verbinden via een eenmalige link?</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -1096,11 +1150,6 @@ <target>Verbindingsfout (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Verbindingsverzoek</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Verbindingsverzoek verzonden!</target> @@ -1206,6 +1255,11 @@ <target>Maak link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Maak een nieuw profiel aan in [desktop-app](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Maak een eenmalige uitnodiging link</target> @@ -1258,17 +1312,17 @@ </trans-unit> <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> - <target>Database ID</target> + <target>Database-ID</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database ID: %d" xml:space="preserve"> <source>Database ID: %d</source> - <target>Database ID: %d</target> + <target>Database-ID: %d</target> <note>copied message info</note> </trans-unit> <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> <source>Database IDs and Transport isolation option.</source> - <target>Database ID's en Transport isolatie optie.</target> + <target>Database-ID's en Transport isolatie optie.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Database downgrade" xml:space="preserve"> @@ -1549,6 +1603,11 @@ <target>Verwijderd om: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Bezorging</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Ontvangstbewijzen zijn uitgeschakeld!</target> @@ -1641,12 +1700,12 @@ </trans-unit> <trans-unit id="Disappears at" xml:space="preserve"> <source>Disappears at</source> - <target>Verdwijnt om</target> + <target>Verdwijnt op</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappears at: %@" xml:space="preserve"> <source>Disappears at: %@</source> - <target>Verdwijnt om: %@</target> + <target>Verdwijnt op: %@</target> <note>copied message info</note> </trans-unit> <trans-unit id="Disconnect" xml:space="preserve"> @@ -1654,6 +1713,11 @@ <target>verbinding verbreken</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Ontdek en sluit je aan bij groepen</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Weergavenaam</target> @@ -1789,6 +1853,16 @@ <target>Database versleutelen?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Versleutel lokale bestanden</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Versleutel opgeslagen bestanden en media</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Versleutelde database</target> @@ -1914,11 +1988,20 @@ <target>Fout bij maken van groep link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Fout bij aanmaken van profiel!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Fout bij het ontsleutelen van bestand</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Fout bij het verwijderen van de chat database</target> @@ -1961,7 +2044,7 @@ </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> - <target>Fout bij het inschakelen van ontvangstbevestiging!</target> + <target>Fout bij het inschakelen van ontvangst bevestiging!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error enabling notifications" xml:space="preserve"> @@ -2039,6 +2122,10 @@ <target>Fout bij het verzenden van e-mail</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Fout bij verzenden van bericht</target> @@ -2046,7 +2133,7 @@ </trans-unit> <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> <source>Error setting delivery receipts!</source> - <target>Fout bij het instellen van ontvangstbevestiging!</target> + <target>Fout bij het instellen van ontvangst bevestiging!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> @@ -2161,12 +2248,12 @@ </trans-unit> <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> <source>File will be received when your contact completes uploading it.</source> - <target>Het bestand wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</target> + <target>Het bestand wordt gedownload wanneer uw contact het uploaden heeft voltooid.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> - <target>Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</target> + <target>Het bestand wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of controleer later!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="File: %@" xml:space="preserve"> @@ -2437,7 +2524,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Geschiedenis</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2476,7 +2563,7 @@ </trans-unit> <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target>Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contactpersoon kan een uitnodiging link delen.</target> + <target>Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contact kan een uitnodiging link delen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> @@ -2501,7 +2588,7 @@ </trans-unit> <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> <source>Image will be received when your contact completes uploading it.</source> - <target>De afbeelding wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</target> + <target>De afbeelding wordt gedownload wanneer uw contact het uploaden heeft voltooid.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> @@ -2547,7 +2634,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>In antwoord op</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2646,9 @@ <target>Incognito modus</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Incognito modus wordt hier niet ondersteund, uw hoofdprofiel wordt naar groepsleden verzonden</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>De incognito modus beschermt de privacy van uw hoofdprofielnaam en afbeelding, voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Incognito -modus beschermt uw privacy met behulp van een nieuw willekeurig profiel voor elk contact.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2723,11 @@ <target>Ongeldig server adres!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Ongeldige status</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Uitnodiging verlopen!</target> @@ -2683,7 +2770,7 @@ </trans-unit> <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> <source>It can happen when you or your connection used the old database backup.</source> - <target>Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte.</target> + <target>Het kan gebeuren wanneer u of de ander een oude databaseback-up gebruikt.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> @@ -2693,7 +2780,7 @@ 3. The connection was compromised.</source> <target>Het kan gebeuren wanneer: 1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server. -2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt. +2. Decodering van het bericht is mislukt, omdat u of uw contact een oude databaseback-up heeft gebruikt. 3. De verbinding is verbroken.</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -2849,7 +2936,7 @@ </trans-unit> <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target>Veel mensen vroegen: *als SimpleX geen gebruikers ID's heeft, hoe kan het dan berichten bezorgen?*</target> + <target>Veel mensen vroegen: *als SimpleX geen gebruikers-ID's heeft, hoe kan het dan berichten bezorgen?*</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Mark deleted for everyone" xml:space="preserve"> @@ -2900,11 +2987,11 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Fout bij bezorging van bericht</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> - <target>Ontvangstbevestiging voor berichten!</target> + <target>Ontvangst bevestiging voor berichten!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> @@ -2987,6 +3074,11 @@ <target>Meer verbeteringen volgen snel!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Hoogstwaarschijnlijk is deze verbinding verwijderd.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd.</target> @@ -3047,6 +3139,11 @@ <target>Nieuw database archief</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Nieuwe desktop app!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Nieuwe weergavenaam</target> @@ -3092,6 +3189,11 @@ <target>Geen contacten om toe te voegen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Geen bezorging informatie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Geen apparaattoken!</target> @@ -3213,7 +3315,7 @@ </trans-unit> <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target>Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</target> + <target>Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only you can make calls." xml:space="preserve"> @@ -3233,12 +3335,12 @@ </trans-unit> <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> <source>Only your contact can add message reactions.</source> - <target>Alleen uw contactpersoon kan berichtreacties toevoegen.</target> + <target>Alleen uw contact kan berichtreacties toevoegen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target>Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</target> + <target>Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can make calls." xml:space="preserve"> @@ -3248,12 +3350,16 @@ </trans-unit> <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> <source>Only your contact can send disappearing messages.</source> - <target>Alleen uw contactpersoon kan verdwijnende berichten verzenden.</target> + <target>Alleen uw contact kan verdwijnende berichten verzenden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> <source>Only your contact can send voice messages.</source> - <target>Alleen uw contactpersoon kan spraak berichten verzenden.</target> + <target>Alleen uw contact kan spraak berichten verzenden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> @@ -3346,10 +3452,10 @@ <target>Plak de ontvangen link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <target>Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon.</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> + <target>Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contact.</target> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3368,12 +3474,12 @@ </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> - <target>Vraag uw contactpersoon om het verzenden van spraak berichten in te schakelen.</target> + <target>Vraag uw contact om het verzenden van spraak berichten in te schakelen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> - <target>Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</target> + <target>Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> @@ -3596,6 +3702,11 @@ <target>Lees meer in onze [GitHub-repository](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Bevestigingen zijn uitgeschakeld</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Ontvangen op</target> @@ -3666,8 +3777,8 @@ <target>Afwijzen</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Contact afwijzen (afzender NIET op de hoogte)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3913,7 +4024,7 @@ </trans-unit> <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> <source>Scan security code from your contact's app.</source> - <target>Scan de beveiligingscode van de app van uw contactpersoon.</target> + <target>Scan de beveiligingscode van de app van uw contact.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Scan server QR code" xml:space="preserve"> @@ -3986,6 +4097,10 @@ <target>Direct bericht sturen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Stuur een verdwijnend bericht</target> @@ -4038,12 +4153,12 @@ </trans-unit> <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> - <target>Het verzenden van ontvangstbevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen.</target> + <target>Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> <source>Sending delivery receipts will be enabled for all contacts.</source> - <target>Het verzenden van ontvangstbevestiging wordt ingeschakeld voor alle contactpersonen.</target> + <target>Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contactpersonen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending file will be stopped." xml:space="preserve"> @@ -4053,12 +4168,22 @@ </trans-unit> <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is disabled for %lld contacts</source> - <target>Het verzenden van ontvangstbevestiging is uitgeschakeld voor %lld-contactpersonen</target> + <target>Het verzenden van ontvangst bevestiging is uitgeschakeld voor %lld-contactpersonen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Het verzenden van bevestigingen is uitgeschakeld voor %LLD -groepen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> - <target>Het verzenden van ontvangstbevestiging is ingeschakeld voor %lld-contactpersonen</target> + <target>Het verzenden van ontvangst bevestiging is ingeschakeld voor %lld-contactpersonen</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Het verzenden van bevestigingen is ingeschakeld voor %LLD -groepen</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> @@ -4201,6 +4326,11 @@ <target>Ontwikkelaars opties tonen</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Laat laatste berichten zien</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Toon voorbeeld</target> @@ -4271,6 +4401,11 @@ <target>Eenmalige SimpleX uitnodiging</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Vereenvoudigde incognitomodus</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Overslaan</target> @@ -4281,6 +4416,11 @@ <target>Overgeslagen berichten</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Kleine groepen (max 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - u kunt de Chat console raadplegen voor meer details.</target> @@ -4463,7 +4603,7 @@ </trans-unit> <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> <source>The 1st platform without any user identifiers – private by design.</source> - <target>Het eerste platform zonder gebruikers ID's, privé door ontwerp.</target> + <target>Het eerste platform zonder gebruikers-ID's, privé door ontwerp.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> @@ -4490,7 +4630,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> </trans-unit> <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> <source>The contact you shared this link with will NOT be able to connect!</source> - <target>Het contact met wie je deze link hebt gedeeld, kan GEEN verbinding maken!</target> + <target>Het contact met wie je deze link hebt gedeeld kan GEEN verbinding maken!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> @@ -4573,8 +4713,8 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Deze instellingen zijn voor uw huidige profiel **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> <target>Ze kunnen worden overschreven in contactinstellingen</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -4593,6 +4733,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> <target>Deze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Deze groep heeft meer dan %lld -leden, ontvangstbevestigingen worden niet verzonden.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Deze groep bestaat niet meer.</target> @@ -4610,12 +4755,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> </trans-unit> <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> <source>To connect, your contact can scan QR code or use the link in the app.</source> - <target>Om verbinding te maken, kan uw contact persoon de QR-code scannen of de link in de app gebruiken.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Om het profiel te vinden dat wordt gebruikt voor een incognito verbinding, tikt u op de naam van het contact of de groep bovenaan de chat.</target> + <target>Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> @@ -4625,7 +4765,7 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</target> </trans-unit> <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target>Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</target> + <target>Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> @@ -4657,7 +4797,12 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc </trans-unit> <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> - <target>Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contactpersoon te verifiëren.</target> + <target>Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Schakel incognito in tijdens het verbinden.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> @@ -4698,7 +4843,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Onverwachte fout: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4753,8 +4898,8 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> - <target>Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. -Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</target> + <target>Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. +Om verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Unlock" xml:space="preserve"> @@ -4837,6 +4982,11 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link <target>Gebruik chat</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Gebruik het huidige profiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Gebruik voor nieuwe verbindingen</target> @@ -4847,6 +4997,11 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link <target>De iOS-oproepinterface gebruiken</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Gebruik een nieuw incognitoprofiel</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Gebruik server</target> @@ -4889,7 +5044,7 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link </trans-unit> <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> - <target>De video wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</target> + <target>De video wordt gedownload wanneer uw contact het uploaden heeft voltooid.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> @@ -5137,9 +5292,9 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link <target>U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> - <target>Je hebt je contactpersoon uitgenodigd</target> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Je hebt je contact uitgenodigd</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group" xml:space="preserve"> @@ -5214,7 +5369,7 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link </trans-unit> <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> - <target>Je probeert een contact met wie je een incognito profiel hebt gedeeld, uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</target> + <target>Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> @@ -5267,11 +5422,6 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link <target>Uw chat profiel wordt verzonden naar de groepsleden</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Uw chat profiel wordt naar uw contactpersoon verzonden</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Uw chat profielen</target> @@ -5280,13 +5430,13 @@ Om verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> <source>Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link).</source> - <target>Uw contactpersoon moet online zijn om de verbinding te voltooien. -U kunt deze verbinding verbreken en het contact verwijderen (en later proberen met een nieuwe link).</target> + <target>Uw contact moet online zijn om de verbinding te voltooien. +U kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> - <target>Uw contactpersoon heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).</target> + <target>Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> @@ -5297,7 +5447,8 @@ U kunt deze verbinding verbreken en het contact verwijderen (en later proberen m <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> <source>Your contacts in SimpleX will see it. You can change it in Settings.</source> - <target>Uw contacten in SimpleX zullen het zien. U kunt dit wijzigen in Instellingen.</target> + <target>Uw contacten in SimpleX kunnen het zien. +U kunt dit wijzigen in Instellingen.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Your contacts will remain connected." xml:space="preserve"> @@ -5325,6 +5476,11 @@ You can change it in Settings.</source> <target>Uw privacy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Uw profiel **%@** wordt gedeeld.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5332,11 +5488,6 @@ SimpleX servers cannot see your profile.</source> SimpleX servers kunnen uw profiel niet zien.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Je profiel wordt verzonden naar het contact van wie je deze link hebt ontvangen</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen.</target> @@ -5424,7 +5575,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="bad message ID" xml:space="preserve"> <source>bad message ID</source> - <target>Onjuiste bericht ID</target> + <target>Onjuiste bericht-ID</target> <note>integrity error chat item</note> </trans-unit> <trans-unit id="bad message hash" xml:space="preserve"> @@ -5502,6 +5653,10 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>verbonden</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>Verbinden</target> @@ -5612,6 +5767,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>direct</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>uitgeschakeld</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>dubbel bericht</target> @@ -5692,6 +5852,11 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>fout</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>gebeurtenis gebeurd</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>groep verwijderd</target> @@ -5719,7 +5884,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> - <target>incognito via contact adres link</target> + <target>incognito via contactadres link</target> <note>chat list item description</note> </trans-unit> <trans-unit id="incognito via group link" xml:space="preserve"> @@ -5953,6 +6118,10 @@ SimpleX servers kunnen uw profiel niet zien.</target> <target>beveiligingscode gewijzigd</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>beginnen…</target> @@ -5985,7 +6154,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="via contact address link" xml:space="preserve"> <source>via contact address link</source> - <target>via contact adres link</target> + <target>via contactadres link</target> <note>chat list item description</note> </trans-unit> <trans-unit id="via group link" xml:space="preserve"> @@ -6097,7 +6266,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6112,7 +6281,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> - <target>SimpleX gebruikt Face ID voor lokale authenticatie</target> + <target>SimpleX gebruikt Face-ID voor lokale authenticatie</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> @@ -6129,7 +6298,7 @@ SimpleX servers kunnen uw profiel niet zien.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="nl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json index c82fe9ae6b..20246f53d4 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "nl", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 2845a11a1c..2e0e2de446 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="pl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 kolorowy!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Historia</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## W odpowiedzi na</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#sekret#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ i %@ połączeni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ o %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ chce się połączyć!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ i %lld innych członków połączeni</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,11 @@ <target>%lld minut</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld nowe języki interfejsu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld sekund(y)</target> @@ -302,6 +332,15 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- połącz do [serwera katalogowego](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- potwierdzenie dostarczenia (do 20 członków). +- szybszy i bardziej stabilny.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +436,9 @@ <target>Nowy kontakt</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Losowy profil zostanie wysłany do kontaktu, od którego otrzymałeś ten link</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Losowy profil zostanie wysłany do Twojego kontaktu</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Nowy losowy profil zostanie udostępniony.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +494,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Akceptuj kontakt</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Zaakceptować prośbę o połączenie?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +507,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Akceptuj incognito</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -680,6 +714,11 @@ <target>Kompilacja aplikacji: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>Aplikacja szyfruje nowe lokalne pliki (bez filmów).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Ikona aplikacji</target> @@ -815,6 +854,11 @@ <target>Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Bułgarski, fiński, tajski i ukraiński – dzięki użytkownikom i [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>Według profilu czatu (domyślnie) lub [według połączenia](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> @@ -1046,9 +1090,19 @@ <target>Połącz</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Połączyć się przez link kontaktowy?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Połącz bezpośrednio</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Połącz incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Połącz przez link kontaktowy</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1120,9 @@ <target>Połącz się przez link / kod QR</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Połączyć się przez jednorazowy link?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Połącz przez jednorazowy link</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1150,6 @@ <target>Błąd połączenia (UWIERZYTELNIANIE)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Prośba o połączenie</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Prośba o połączenie wysłana!</target> @@ -1206,6 +1255,11 @@ <target>Utwórz link</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Utwórz nowy profil w [aplikacji desktopowej](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Utwórz jednorazowy link do zaproszenia</target> @@ -1549,6 +1603,11 @@ <target>Usunięto o: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Dostarczenie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Potwierdzenia dostawy są wyłączone!</target> @@ -1654,6 +1713,11 @@ <target>Rozłącz</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Odkrywaj i dołączaj do grup</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Wyświetlana nazwa</target> @@ -1789,6 +1853,16 @@ <target>Zaszyfrować bazę danych?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Zaszyfruj lokalne pliki</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Szyfruj przechowywane pliki i media</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>Zaszyfrowana baza danych</target> @@ -1914,11 +1988,20 @@ <target>Błąd tworzenia linku grupy</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Błąd tworzenia profilu!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Błąd odszyfrowania pliku</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Błąd usuwania bazy danych czatu</target> @@ -2039,6 +2122,10 @@ <target>Błąd wysyłania e-mail</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Błąd wysyłania wiadomości</target> @@ -2437,7 +2524,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>Historia</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2634,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>W odpowiedzi na</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2646,9 @@ <target>Tryb incognito</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Tryb Incognito nie jest tutaj obsługiwany - główny profil zostanie wysłany do członków grupy</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>Tryb incognito chroni prywatność nazwy i obrazu głównego profilu — dla każdego nowego kontaktu tworzony jest nowy losowy profil.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Tryb incognito chroni Twoją prywatność używając nowego losowego profilu dla każdego kontaktu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2723,11 @@ <target>Nieprawidłowy adres serwera!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Nieprawidłowy status</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Zaproszenie wygasło!</target> @@ -2900,7 +2987,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Błąd dostarczenia wiadomości</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3074,11 @@ <target>Więcej ulepszeń już wkrótce!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Najprawdopodobniej to połączenie jest usunięte.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Najprawdopodobniej ten kontakt usunął połączenie z Tobą.</target> @@ -3047,6 +3139,11 @@ <target>Nowe archiwum bazy danych</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Nowa aplikacja desktopowa!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Nowa wyświetlana nazwa</target> @@ -3092,6 +3189,11 @@ <target>Brak kontaktów do dodania</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Brak informacji dostawy</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Brak tokenu urządzenia!</target> @@ -3256,6 +3358,10 @@ <target>Tylko Twój kontakt może wysyłać wiadomości głosowe.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Otwórz Ustawienia</target> @@ -3346,10 +3452,10 @@ <target>Wklej otrzymany link</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3363,7 +3469,7 @@ </trans-unit> <trans-unit id="Permanent decryption error" xml:space="preserve"> <source>Permanent decryption error</source> - <target>Błąd odszyfrowania</target> + <target>Stały błąd odszyfrowania</target> <note>message decrypt error item</note> </trans-unit> <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> @@ -3596,6 +3702,11 @@ <target>Przeczytaj więcej na naszym [repozytorium GitHub](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Potwierdzenia są wyłączone</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Otrzymane o</target> @@ -3666,8 +3777,8 @@ <target>Odrzuć</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Odrzuć kontakt (nadawca NIE został powiadomiony)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3986,6 +4097,10 @@ <target>Wyślij wiadomość bezpośrednią</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Wyślij znikającą wiadomość</target> @@ -4056,11 +4171,21 @@ <target>Wysyłanie potwierdzeń jest wyłączone dla %lld kontaktów</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Wysyłanie potwierdzeń jest wyłączone dla %lld grup</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>Wysyłanie potwierdzeń jest włączone dla %lld kontaktów</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Wysyłanie potwierdzeń jest włączone dla %lld grup</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Wysyłanie przez</target> @@ -4201,6 +4326,11 @@ <target>Pokaż opcje dewelopera</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Pokaż ostatnie wiadomości</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Pokaż podgląd</target> @@ -4271,6 +4401,11 @@ <target>Zaproszenie jednorazowe SimpleX</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Uproszczony tryb incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Pomiń</target> @@ -4281,6 +4416,11 @@ <target>Pominięte wiadomości</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Małe grupy (maks. 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu.</target> @@ -4573,9 +4713,9 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Te ustawienia dotyczą Twojego bieżącego profilu **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Można je nadpisać w ustawieniach kontaktu</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Można je nadpisać w ustawieniach kontaktu.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4733,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Tego działania nie można cofnąć - Twój profil, kontakty, wiadomości i pliki zostaną nieodwracalnie utracone.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Ta grupa już nie istnieje.</target> @@ -4613,11 +4758,6 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom <target>Aby się połączyć, Twój kontakt może zeskanować kod QR lub skorzystać z linku w aplikacji.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Aby znaleźć profil używany do połączenia incognito, dotknij nazwę kontaktu lub grupy w górnej części czatu.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Aby nawiązać nowe połączenie</target> @@ -4660,6 +4800,11 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <target>Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Przełącz incognito przy połączeniu.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Izolacja transportu</target> @@ -4698,7 +4843,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.</ta <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Nieoczekiwany błąd: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4837,6 +4982,11 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Użyj czatu</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Użyj obecnego profilu</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Użyj dla nowych połączeń</target> @@ -4847,6 +4997,11 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Użyj interfejsu połączeń iOS</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Użyj nowego profilu incognito</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Użyj serwera</target> @@ -5137,8 +5292,8 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>Zaprosiłeś swój kontakt</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5267,11 +5422,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc <target>Twój profil czatu zostanie wysłany do członków grupy</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Twój profil czatu zostanie wysłany do Twojego kontaktu</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Twoje profile czatu</target> @@ -5326,6 +5476,11 @@ Możesz to zmienić w Ustawieniach.</target> <target>Twoja prywatność</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Twój profil **%@** zostanie udostępniony.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5333,11 +5488,6 @@ SimpleX servers cannot see your profile.</source> Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Twój profil zostanie wysłany do kontaktu, od którego otrzymałeś ten link</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu.</target> @@ -5503,6 +5653,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>połączony</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>łączenie</target> @@ -5613,6 +5767,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>bezpośredni</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>wyłączony</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>zduplikowana wiadomość</target> @@ -5693,6 +5852,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>błąd</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>nowe wydarzenie</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>grupa usunięta</target> @@ -5954,6 +6118,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> <target>kod bezpieczeństwa zmieniony</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>uruchamianie…</target> @@ -6098,7 +6266,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6130,7 +6298,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="pl" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json index d4b2ca0bb5..22043b831d 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "pl", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file 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 245bd51b23..54841f241e 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="ru" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 цвет!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## История</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## В ответ на</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#секрет#</target> @@ -72,6 +87,11 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ и %@ соединены</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <target>%1$@ в %2$@:</target> @@ -102,6 +122,11 @@ <target>%@ хочет соединиться!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ и %lld других членов соединены</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -172,6 +197,11 @@ <target>%lld минуты</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld новых языков интерфейса</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld секунд</target> @@ -302,6 +332,15 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <target>- соединиться с [каталогом групп](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- отчеты о доставке (до 20 членов). +- быстрее и стабильнее.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. @@ -397,14 +436,9 @@ <target>Новый контакт</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>Контакту, от которого Вы получили эту ссылку, будет отправлен случайный профиль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>Вашему контакту будет отправлен случайный профиль</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Будет отправлен новый случайный профиль.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -460,9 +494,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>Принять запрос</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Принять запрос?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -473,7 +507,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>Принять инкогнито</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -680,6 +714,11 @@ <target>Сборка приложения: %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <target>Приложение шифрует новые локальные файлы (кроме видео).</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>Иконка</target> @@ -815,6 +854,11 @@ <target>Вы и Ваш контакт можете отправлять голосовые сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА).</target> @@ -1046,9 +1090,19 @@ <target>Соединиться</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>Соединиться через ссылку-контакт?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Соединиться напрямую</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Соединиться Инкогнито</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Соединиться через ссылку-контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1066,9 +1120,9 @@ <target>Соединиться через ссылку / QR код</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>Соединиться через одноразовую ссылку?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Соединиться через одноразовую ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1096,11 +1150,6 @@ <target>Ошибка соединения (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>Запрос на соединение</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>Запрос на соединение отправлен!</target> @@ -1206,6 +1255,11 @@ <target>Создать ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <target>Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>Создать ссылку-приглашение</target> @@ -1549,6 +1603,11 @@ <target>Удалено: %@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Доставка</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> <target>Отчёты о доставке выключены!</target> @@ -1654,6 +1713,11 @@ <target>Разрыв соединения</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <target>Найдите и вступите в группы</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>Имя профиля</target> @@ -1789,6 +1853,16 @@ <target>Зашифровать базу данных?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>Шифровать локальные файлы</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <target>Шифруйте сохраненные файлы и медиа</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>База данных зашифрована</target> @@ -1914,11 +1988,20 @@ <target>Ошибка при создании ссылки группы</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>Ошибка создания профиля!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>Ошибка расшифровки файла</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>Ошибка при удалении данных чата</target> @@ -2039,6 +2122,10 @@ <target>Ошибка отправки email</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>Ошибка при отправке сообщения</target> @@ -2437,7 +2524,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>История</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2547,7 +2634,7 @@ <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> <target>В ответ на</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2559,14 +2646,9 @@ <target>Режим Инкогнито</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>Режим Инкогнито здесь не поддерживается - Ваш основной профиль будет отправлен членам группы</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>Режим Инкогнито защищает конфиденциальность имени и изображения Вашего основного профиля — для каждого нового контакта создается новый случайный профиль.</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создается новый случайный профиль.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2641,6 +2723,11 @@ <target>Ошибка в адресе сервера!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Неверный статус</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>Приглашение истекло!</target> @@ -2900,7 +2987,7 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>Ошибка доставки сообщения</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> @@ -2987,6 +3074,11 @@ <target>Дополнительные улучшения скоро!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Скорее всего, соединение удалено.</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>Скорее всего, этот контакт удалил соединение с Вами.</target> @@ -3047,6 +3139,11 @@ <target>Новый архив чата</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <target>Приложение для компьютера!</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>Новое имя</target> @@ -3092,6 +3189,11 @@ <target>Нет контактов для добавления</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Нет информации от доставке</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>Отсутствует токен устройства!</target> @@ -3256,6 +3358,10 @@ <target>Только Ваш контакт может отправлять голосовые сообщения.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>Открыть Настройки</target> @@ -3346,10 +3452,10 @@ <target>Вставить полученную ссылку</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>Чтобы соединиться, вставьте ссылку, полученную от Вашего контакта.</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3596,6 +3702,11 @@ <target>Узнайте больше из нашего [GitHub репозитория](https://github.com/simplex-chat/simplex-chat#readme).</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Отчёты о доставке выключены</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>Получено</target> @@ -3666,8 +3777,8 @@ <target>Отклонить</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>Отклонить (не уведомляя отправителя)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3986,6 +4097,10 @@ <target>Отправить сообщение</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>Отправить исчезающее сообщение</target> @@ -4056,11 +4171,21 @@ <target>Отправка отчётов о доставке выключена для %lld контактов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Отчёты о доставке выключены для %lld групп</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> <target>Отправка отчётов о доставке включена для %lld контактов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Отчёты о доставке включены для %lld групп</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> <target>Отправка через</target> @@ -4201,6 +4326,11 @@ <target>Показать опции для девелоперов</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Показывать последние сообщения</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>Показывать уведомления</target> @@ -4271,6 +4401,11 @@ <target>SimpleX одноразовая ссылка</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <target>Упрощенный режим Инкогнито</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>Пропустить</target> @@ -4281,6 +4416,11 @@ <target>Пропущенные сообщения</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Маленькие группы (до 20)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли.</target> @@ -4573,9 +4713,9 @@ It can happen because of some bug or when the connection is compromised.</source <target>Установки для Вашего активного профиля **%@**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> - <target>Они могут быть переопределены в настройках контактов</target> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Они могут быть переопределены в настройках контактов и групп.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4593,6 +4733,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>Это действие нельзя отменить — Ваш профиль, контакты, сообщения и файлы будут безвозвратно утеряны.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>В группе более %lld членов, отчёты о доставке выключены.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>Эта группа больше не существует.</target> @@ -4613,11 +4758,6 @@ It can happen because of some bug or when the connection is compromised.</source <target>Чтобы соединиться с Вами, Ваш контакт может отсканировать QR-код или использовать ссылку в приложении.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>Чтобы соединиться</target> @@ -4660,6 +4800,11 @@ You will be prompted to complete authentication before this feature is enabled.< <target>Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах.</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <target>Установите режим Инкогнито при соединении.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>Отдельные сессии для</target> @@ -4698,7 +4843,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>Неожиданная ошибка: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4837,6 +4982,11 @@ To connect, please ask your contact to create another connection link and check <target>Использовать чат</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Использовать активный профиль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>Использовать для новых соединений</target> @@ -4847,6 +4997,11 @@ To connect, please ask your contact to create another connection link and check <target>Использовать интерфейс iOS для звонков</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Использовать новый Инкогнито профиль</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>Использовать сервер</target> @@ -5137,9 +5292,9 @@ To connect, please ask your contact to create another connection link and check <target>Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> - <target>Вы пригласили Ваш контакт</target> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Вы пригласили контакт</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You joined this group" xml:space="preserve"> @@ -5267,11 +5422,6 @@ To connect, please ask your contact to create another connection link and check <target>Ваш профиль чата будет отправлен членам группы</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>Ваш профиль будет отправлен Вашему контакту</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>Ваши профили чата</target> @@ -5326,6 +5476,11 @@ You can change it in Settings.</source> <target>Конфиденциальность</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Будет отправлен Ваш профиль **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5333,11 +5488,6 @@ SimpleX servers cannot see your profile.</source> SimpleX серверы не могут получить доступ к Вашему профилю.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>Ваш профиль будет отправлен Вашему контакту.</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве.</target> @@ -5503,6 +5653,10 @@ SimpleX серверы не могут получить доступ к Ваше <target>соединение установлено</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>соединяется</target> @@ -5613,6 +5767,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>прямое</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>выключено</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>повторное сообщение</target> @@ -5693,6 +5852,11 @@ SimpleX серверы не могут получить доступ к Ваше <target>ошибка</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>событие произошло</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>группа удалена</target> @@ -5825,7 +5989,7 @@ SimpleX серверы не могут получить доступ к Ваше </trans-unit> <trans-unit id="moderated" xml:space="preserve"> <source>moderated</source> - <target>удалено</target> + <target>модерировано</target> <note>moderated chat item</note> </trans-unit> <trans-unit id="moderated by %@" xml:space="preserve"> @@ -5954,6 +6118,10 @@ SimpleX серверы не могут получить доступ к Ваше <target>код безопасности изменился</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>инициализация…</target> @@ -6098,7 +6266,7 @@ SimpleX серверы не могут получить доступ к Ваше </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6130,7 +6298,7 @@ SimpleX серверы не могут получить доступ к Ваше </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="ru" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index f271d2b375..2d5d76dd8f 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ru", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..9440816a5b --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "th" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 78945d08a1..19681b3150 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2,5870 +2,6274 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="th" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (" xml:space="preserve" approved="no"> + <trans-unit id=" (" xml:space="preserve"> <source> (</source> - <target state="translated"> (</target> + <target> (</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> + <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> - <target state="translated"> (สามารถคัดลอกได้)</target> + <target> (สามารถคัดลอกได้)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> + <trans-unit id="!1 colored!" xml:space="preserve"> <source>!1 colored!</source> - <target state="translated">!1 มีสี!</target> + <target>!1 มีสี!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve" approved="no"> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <note>copied message info</note> + </trans-unit> + <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> - <target state="translated">#ความลับ#</target> + <target>#ความลับ#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve" approved="no"> + <trans-unit id="%@" xml:space="preserve"> <source>%@</source> - <target state="translated">%@</target> + <target>%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve" approved="no"> + <trans-unit id="%@ %@" xml:space="preserve"> <source>%@ %@</source> - <target state="translated">%@ %@</target> + <target>%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <trans-unit id="%@ (current)" xml:space="preserve"> <source>%@ (current)</source> - <target state="translated">%@ (ปัจจุบัน)</target> + <target>%@ (ปัจจุบัน)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <trans-unit id="%@ (current):" xml:space="preserve"> <source>%@ (current):</source> - <target state="translated">%@ (ปัจจุบัน):</target> + <target>%@ (ปัจจุบัน):</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> + <trans-unit id="%@ / %@" xml:space="preserve"> <source>%@ / %@</source> - <target state="translated">%@ / %@</target> + <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve"> + <source>%1$@ at %2$@:</source> + <target>%1$@ ที่ %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="%@ is connected!" xml:space="preserve"> <source>%@ is connected!</source> - <target state="translated">%@ เชื่อมต่อสำเร็จ!</target> + <target>%@ เชื่อมต่อสำเร็จ!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> + <trans-unit id="%@ is not verified" xml:space="preserve"> <source>%@ is not verified</source> - <target state="translated">%@ ไม่ได้รับการยืนยัน</target> + <target>%@ ไม่ได้รับการยืนยัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> + <trans-unit id="%@ is verified" xml:space="preserve"> <source>%@ is verified</source> - <target state="translated">%@ ได้รับการยืนยันแล้ว</target> + <target>%@ ได้รับการตรวจสอบแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve" approved="no"> + <trans-unit id="%@ servers" xml:space="preserve"> <source>%@ servers</source> - <target state="translated">%@ เซิร์ฟเวอร์</target> + <target>%@ เซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> + <trans-unit id="%@ wants to connect!" xml:space="preserve"> <source>%@ wants to connect!</source> - <target state="translated">%@ อยากเชื่อมต่อ!</target> + <target>%@ อยากเชื่อมต่อ!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@:" xml:space="preserve" approved="no"> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> - <target state="translated">%@:</target> + <target>%@:</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%d days" xml:space="preserve" approved="no"> + <trans-unit id="%d days" xml:space="preserve"> <source>%d days</source> - <target state="translated">%d วัน</target> + <target>%d วัน</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d hours" xml:space="preserve" approved="no"> + <trans-unit id="%d hours" xml:space="preserve"> <source>%d hours</source> - <target state="translated">%d ชั่วโมง</target> + <target>%d ชั่วโมง</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d min" xml:space="preserve" approved="no"> + <trans-unit id="%d min" xml:space="preserve"> <source>%d min</source> - <target state="translated">%d นาที</target> + <target>%d นาที</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d months" xml:space="preserve" approved="no"> + <trans-unit id="%d months" xml:space="preserve"> <source>%d months</source> - <target state="translated">%d เดือน</target> + <target>%d เดือน</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d sec" xml:space="preserve" approved="no"> + <trans-unit id="%d sec" xml:space="preserve"> <source>%d sec</source> - <target state="translated">%d วินาที</target> + <target>%d วินาที</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> + <trans-unit id="%d skipped message(s)" xml:space="preserve"> <source>%d skipped message(s)</source> - <target state="translated">%d ข้อความที่ถูกข้าม</target> + <target>%d ข้อความที่ถูกข้าม</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="%d weeks" xml:space="preserve" approved="no"> + <trans-unit id="%d weeks" xml:space="preserve"> <source>%d weeks</source> - <target state="translated">%d สัปดาห์</target> + <target>%d สัปดาห์</target> <note>time interval</note> </trans-unit> - <trans-unit id="%lld" xml:space="preserve" approved="no"> + <trans-unit id="%lld" xml:space="preserve"> <source>%lld</source> - <target state="translated">%lld</target> + <target>%lld</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve" approved="no"> + <trans-unit id="%lld %@" xml:space="preserve"> <source>%lld %@</source> - <target state="translated">%lld %@</target> + <target>%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve"> <source>%lld contact(s) selected</source> - <target state="translated">% ผู้ติดต่อ LLD ที่เลือกไว้</target> + <target>% ผู้ติดต่อ LLD ที่เลือกไว้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> <source>%lld file(s) with total size of %@</source> - <target state="translated">%lld ไฟล์ที่มีขนาดรวม %@</target> + <target>%lld ไฟล์ที่มีขนาดรวม %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld members" xml:space="preserve" approved="no"> + <trans-unit id="%lld members" xml:space="preserve"> <source>%lld members</source> - <target state="translated">%lld สมาชิก</target> + <target>%lld สมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> + <trans-unit id="%lld minutes" xml:space="preserve"> <source>%lld minutes</source> - <target state="translated">%lld นาที</target> + <target>%lld นาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> - <target state="translated">%lld วินาที</target> + <target>%lld วินาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> + <trans-unit id="%lld seconds" xml:space="preserve"> <source>%lld seconds</source> - <target state="translated">%lld วินาที</target> + <target>%lld วินาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldd" xml:space="preserve" approved="no"> + <trans-unit id="%lldd" xml:space="preserve"> <source>%lldd</source> - <target state="translated">%lldd</target> + <target>%lldd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldh" xml:space="preserve" approved="no"> + <trans-unit id="%lldh" xml:space="preserve"> <source>%lldh</source> - <target state="translated">%lldh</target> + <target>%lldh</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldk" xml:space="preserve" approved="no"> + <trans-unit id="%lldk" xml:space="preserve"> <source>%lldk</source> - <target state="translated">%lldk</target> + <target>%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldm" xml:space="preserve" approved="no"> + <trans-unit id="%lldm" xml:space="preserve"> <source>%lldm</source> - <target state="translated">%lldm</target> + <target>%lldm</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve" approved="no"> + <trans-unit id="%lldmth" xml:space="preserve"> <source>%lldmth</source> - <target state="translated">%lldmth</target> + <target>%lldmth</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%llds" xml:space="preserve" approved="no"> + <trans-unit id="%llds" xml:space="preserve"> <source>%llds</source> - <target state="translated">%llds</target> + <target>%llds</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldw" xml:space="preserve" approved="no"> + <trans-unit id="%lldw" xml:space="preserve"> <source>%lldw</source> - <target state="translated">%lldw</target> + <target>%lldw</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> <source>%u messages failed to decrypt.</source> - <target state="translated">การ decrypt %u ข้อความล้มเหลว</target> + <target>การ decrypt %u ข้อความล้มเหลว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> + <trans-unit id="%u messages skipped." xml:space="preserve"> <source>%u messages skipped.</source> - <target state="translated">%u ข้อความที่ถูกข้าม</target> + <target>%u ข้อความที่ถูกข้าม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve" approved="no"> + <trans-unit id="(" xml:space="preserve"> <source>(</source> - <target state="translated">(</target> + <target>(</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve" approved="no"> + <trans-unit id=")" xml:space="preserve"> <source>)</source> - <target state="translated">)</target> + <target>)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> + <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ</target> + <target>**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> + <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> <source>**Create link / QR code** for your contact to use.</source> - <target state="translated">**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้</target> + <target>**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> + <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target state="translated">**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี</target> + <target>**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> + <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target state="translated">**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)</target> + <target>**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve" approved="no"> + <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target state="translated">**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**</target> + <target>**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no"> + <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <target state="translated">**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target> + <target>**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> + <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <target state="translated">**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง</target> + <target>**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve" approved="no"> + <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target state="translated">**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ</target> + <target>**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no"> + <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <target state="translated">**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain</target> + <target>**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no"> + <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> <source>**e2e encrypted** audio call</source> - <target state="translated">**encrypted จากต้นจนจบ** โทรด้วยเสียง</target> + <target>การโทรเสียงแบบ **encrypted จากต้นจนจบ**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve" approved="no"> + <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> <source>**e2e encrypted** video call</source> - <target state="translated">**encrypted จากต้นจนจบ** การสนทนาทางวิดีโอ</target> + <target>**encrypted จากต้นจนจบ** การสนทนาทางวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="*bold*" xml:space="preserve" approved="no"> + <trans-unit id="*bold*" xml:space="preserve"> <source>\*bold*</source> - <target state="translated">\*ตัวหนา*</target> + <target>\*ตัวหนา*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=", " xml:space="preserve" approved="no"> + <trans-unit id=", " xml:space="preserve"> <source>, </source> - <target state="translated">, </target> + <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve" approved="no"> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target>- การส่งข้อความมีเสถียรภาพมากขึ้น +- กลุ่มที่ดีขึ้นเล็กน้อย +- และอื่น ๆ!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> <source>- voice messages up to 5 minutes. - custom time to disappear. - editing history.</source> - <target state="translated">- ข้อความเสียงนานสุด 5 นาที + <target>- ข้อความเสียงนานสุด 5 นาที - เวลาที่กำหนดเองที่จะหายไป - ประวัติการแก้ไข</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="." xml:space="preserve" approved="no"> + <trans-unit id="." xml:space="preserve"> <source>.</source> - <target state="translated">.</target> + <target>.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="0s" xml:space="preserve" approved="no"> + <trans-unit id="0s" xml:space="preserve"> <source>0s</source> - <target state="translated">0s</target> + <target>0s</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="1 day" xml:space="preserve" approved="no"> + <trans-unit id="1 day" xml:space="preserve"> <source>1 day</source> - <target state="translated">1 วัน</target> + <target>1 วัน</target> <note>time interval</note> </trans-unit> - <trans-unit id="1 hour" xml:space="preserve" approved="no"> + <trans-unit id="1 hour" xml:space="preserve"> <source>1 hour</source> - <target state="translated">1 ชั่วโมง</target> + <target>1 ชั่วโมง</target> <note>time interval</note> </trans-unit> - <trans-unit id="1 minute" xml:space="preserve" approved="no"> + <trans-unit id="1 minute" xml:space="preserve"> <source>1 minute</source> - <target state="translated">1 นาที</target> + <target>1 นาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="1 month" xml:space="preserve" approved="no"> + <trans-unit id="1 month" xml:space="preserve"> <source>1 month</source> - <target state="translated">1 เดือน</target> + <target>1 เดือน</target> <note>time interval</note> </trans-unit> - <trans-unit id="1 week" xml:space="preserve" approved="no"> + <trans-unit id="1 week" xml:space="preserve"> <source>1 week</source> - <target state="translated">1 สัปดาห์</target> + <target>1 สัปดาห์</target> <note>time interval</note> </trans-unit> - <trans-unit id="1-time link" xml:space="preserve" approved="no"> + <trans-unit id="1-time link" xml:space="preserve"> <source>1-time link</source> - <target state="translated">ลิงก์สำหรับใช้ 1 ครั้ง</target> + <target>ลิงก์สำหรับใช้ 1 ครั้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="5 minutes" xml:space="preserve" approved="no"> + <trans-unit id="5 minutes" xml:space="preserve"> <source>5 minutes</source> - <target state="translated">5 นาที</target> + <target>5 นาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="6" xml:space="preserve" approved="no"> + <trans-unit id="6" xml:space="preserve"> <source>6</source> - <target state="translated">6</target> + <target>6</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="30 seconds" xml:space="preserve" approved="no"> + <trans-unit id="30 seconds" xml:space="preserve"> <source>30 seconds</source> - <target state="translated">30 วินาที</target> + <target>30 วินาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=": " xml:space="preserve" approved="no"> + <trans-unit id=": " xml:space="preserve"> <source>: </source> - <target state="translated">: </target> + <target>: </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve" approved="no"> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> - <target state="translated"><p>สวัสดี!</p> + <target><p>สวัสดี!</p> <p><a href="%@">เชื่อมต่อกับฉันผ่าน SimpleX Chat</a></p></target> <note>email text</note> </trans-unit> - <trans-unit id="A new contact" xml:space="preserve" approved="no"> + <trans-unit id="A few more things" xml:space="preserve"> + <source>A few more things</source> + <target>อีกสองสามอย่าง</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="A new contact" xml:space="preserve"> <source>A new contact</source> - <target state="translated">ผู้ติดต่อใหม่</target> + <target>ผู้ติดต่อใหม่</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve" approved="no"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target state="translated">โปรไฟล์แบบสุ่มจะถูกส่งไปยังผู้ติดต่อที่คุณได้รับลิงก์นี้จาก</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve" approved="no"> - <source>A random profile will be sent to your contact</source> - <target state="translated">โปรไฟล์แบบสุ่มจะถูกส่งไปยังผู้ติดต่อของคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve" approved="no"> + <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> - <target state="translated">การเชื่อมต่อ TCP แบบแยกต่างหากจะถูกใช้ **สําหรับแต่ละโปรไฟล์การแชทที่คุณมีในแอป**.</target> + <target>การเชื่อมต่อ TCP แบบแยกต่างหากจะถูกใช้ **สําหรับแต่ละโปรไฟล์การแชทที่คุณมีในแอป**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve" approved="no"> + <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> <source>A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> - <target state="translated">การเชื่อมต่อ TCP แบบแยกต่างหากจะถูกใช้ **สำหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่ม**. + <target>การเชื่อมต่อ TCP แบบแยกต่างหากจะถูกใช้ **สำหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่ม**. **โปรดทราบ**: หากคุณมีการเชื่อมต่อจำนวนมาก แบตเตอรี่และปริมาณการใช้ข้อมูลของคุณอาจสูงขึ้นอย่างมาก และการเชื่อมต่อบางอย่างอาจล้มเหลว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve" approved="no"> + <trans-unit id="Abort" xml:space="preserve"> + <source>Abort</source> + <target>ยกเลิก</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort changing address" xml:space="preserve"> + <source>Abort changing address</source> + <target>ยกเลิกการเปลี่ยนที่อยู่</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Abort changing address?" xml:space="preserve"> + <source>Abort changing address?</source> + <target>ยกเลิกการเปลี่ยนที่อยู่?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About SimpleX" xml:space="preserve"> <source>About SimpleX</source> - <target state="translated">เกี่ยวกับ SimpleX</target> + <target>เกี่ยวกับ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX Chat" xml:space="preserve" approved="no"> + <trans-unit id="About SimpleX Chat" xml:space="preserve"> <source>About SimpleX Chat</source> - <target state="translated">เกี่ยวกับ SimpleX Chat</target> + <target>เกี่ยวกับ SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve" approved="no"> + <trans-unit id="About SimpleX address" xml:space="preserve"> <source>About SimpleX address</source> - <target state="translated">เกี่ยวกับที่อยู่ SimpleX</target> + <target>เกี่ยวกับที่อยู่ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accent color" xml:space="preserve" approved="no"> + <trans-unit id="Accent color" xml:space="preserve"> <source>Accent color</source> - <target state="translated">สีเน้น</target> + <target>สีเน้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accept" xml:space="preserve" approved="no"> + <trans-unit id="Accept" xml:space="preserve"> <source>Accept</source> - <target state="translated">รับ</target> + <target>รับ</target> <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve" approved="no"> - <source>Accept contact</source> - <target state="translated">ยอมรับการติดต่อ</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Accept contact request from %@?" xml:space="preserve" approved="no"> + <trans-unit id="Accept contact request from %@?" xml:space="preserve"> <source>Accept contact request from %@?</source> - <target state="translated">รับการขอติดต่อจาก %@?</target> + <target>รับการขอติดต่อจาก %@?</target> <note>notification body</note> </trans-unit> - <trans-unit id="Accept incognito" xml:space="preserve" approved="no"> + <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> - <target state="translated">ยอมรับโหมดไม่ระบุตัวตน</target> - <note>No comment provided by engineer.</note> + <target>ยอมรับโหมดไม่ระบุตัวตน</target> + <note>accept contact request via notification</note> </trans-unit> - <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> - <target state="translated">เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target> + <target>เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve" approved="no"> + <trans-unit id="Add preset servers" xml:space="preserve"> <source>Add preset servers</source> - <target state="translated">เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> + <target>เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add profile" xml:space="preserve" approved="no"> + <trans-unit id="Add profile" xml:space="preserve"> <source>Add profile</source> - <target state="translated">เพิ่มโปรไฟล์</target> + <target>เพิ่มโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve" approved="no"> + <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> <source>Add servers by scanning QR codes.</source> - <target state="translated">เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด</target> + <target>เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add server…" xml:space="preserve" approved="no"> + <trans-unit id="Add server…" xml:space="preserve"> <source>Add server…</source> - <target state="translated">เพิ่มเซิร์ฟเวอร์…</target> + <target>เพิ่มเซิร์ฟเวอร์…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add to another device" xml:space="preserve" approved="no"> + <trans-unit id="Add to another device" xml:space="preserve"> <source>Add to another device</source> - <target state="translated">เพิ่มเข้าไปในอุปกรณ์อื่น</target> + <target>เพิ่มเข้าไปในอุปกรณ์อื่น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add welcome message" xml:space="preserve" approved="no"> + <trans-unit id="Add welcome message" xml:space="preserve"> <source>Add welcome message</source> - <target state="translated">เพิ่มข้อความต้อนรับ</target> + <target>เพิ่มข้อความต้อนรับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Address" xml:space="preserve" approved="no"> + <trans-unit id="Address" xml:space="preserve"> <source>Address</source> - <target state="translated">ที่อยู่</target> + <target>ที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Admins can create the links to join groups." xml:space="preserve" approved="no"> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> + <source>Address change will be aborted. Old receiving address will be used.</source> + <target>การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> <source>Admins can create the links to join groups.</source> - <target state="translated">ผู้ดูแลระบบสามารถสร้างลิงก์เพื่อเข้าร่วมกลุ่มต่างๆได้</target> + <target>ผู้ดูแลระบบสามารถสร้างลิงก์เพื่อเข้าร่วมกลุ่มต่างๆได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Advanced network settings" xml:space="preserve" approved="no"> + <trans-unit id="Advanced network settings" xml:space="preserve"> <source>Advanced network settings</source> - <target state="translated">การตั้งค่าระบบเครือข่ายขั้นสูง</target> + <target>การตั้งค่าระบบเครือข่ายขั้นสูง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All app data is deleted." xml:space="preserve" approved="no"> + <trans-unit id="All app data is deleted." xml:space="preserve"> <source>All app data is deleted.</source> - <target state="translated">ข้อมูลแอปทั้งหมดถูกลบแล้ว.</target> + <target>ข้อมูลแอปทั้งหมดถูกลบแล้ว.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>All chats and messages will be deleted - this cannot be undone!</source> - <target state="translated">แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้!</target> + <target>แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no"> + <trans-unit id="All data is erased when it is entered." xml:space="preserve"> <source>All data is erased when it is entered.</source> - <target state="translated">ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน</target> + <target>ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve" approved="no"> + <trans-unit id="All group members will remain connected." xml:space="preserve"> <source>All group members will remain connected.</source> - <target state="translated">สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่.</target> + <target>สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no"> + <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> - <target state="translated">ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น.</target> + <target>ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no"> + <trans-unit id="All your contacts will remain connected." xml:space="preserve"> <source>All your contacts will remain connected.</source> - <target state="translated">ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่.</target> + <target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> - <target state="translated">ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ.</target> + <target>ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow" xml:space="preserve" approved="no"> + <trans-unit id="Allow" xml:space="preserve"> <source>Allow</source> - <target state="translated">อนุญาต</target> + <target>อนุญาต</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no"> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> <source>Allow calls only if your contact allows them.</source> - <target state="translated">อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น.</target> + <target>อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve" approved="no"> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> <source>Allow disappearing messages only if your contact allows it to you.</source> - <target state="translated">อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น.</target> + <target>อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve" approved="no"> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target state="translated">อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น</target> + <target>อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve" approved="no"> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> <source>Allow message reactions only if your contact allows them.</source> - <target state="translated">อนุญาตการแสดงปฏิกิริยาต่อข้อความเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น</target> + <target>อนุญาตการแสดงปฏิกิริยาต่อข้อความเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Allow message reactions." xml:space="preserve"> <source>Allow message reactions.</source> - <target state="translated">อนุญาตการแสดงปฏิกิริยาต่อข้อความ</target> + <target>อนุญาตการแสดงปฏิกิริยาต่อข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> <source>Allow sending direct messages to members.</source> - <target state="translated">อนุญาตการส่งข้อความโดยตรงไปยังสมาชิก</target> + <target>อนุญาตการส่งข้อความโดยตรงไปยังสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> <source>Allow sending disappearing messages.</source> - <target state="translated">อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target> + <target>อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve" approved="no"> + <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> <source>Allow to irreversibly delete sent messages.</source> - <target state="translated">อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร</target> + <target>อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Allow to send files and media." xml:space="preserve"> + <source>Allow to send files and media.</source> + <target>อนุญาตให้ส่งไฟล์และสื่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Allow to send voice messages." xml:space="preserve"> <source>Allow to send voice messages.</source> - <target state="translated">อนุญาตให้ส่งข้อความเสียง</target> + <target>อนุญาตให้ส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no"> + <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> <source>Allow voice messages only if your contact allows them.</source> - <target state="translated">อนุญาตข้อความเสียงเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น</target> + <target>อนุญาตข้อความเสียงเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve" approved="no"> + <trans-unit id="Allow voice messages?" xml:space="preserve"> <source>Allow voice messages?</source> - <target state="translated">อนุญาตข้อความเสียงหรือไม่?</target> + <target>อนุญาตข้อความเสียงหรือไม่?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> <source>Allow your contacts adding message reactions.</source> - <target state="translated">อนุญาตให้ผู้ติดต่อของคุณเพิ่มการแสดงปฏิกิริยาต่อข้อความ</target> + <target>อนุญาตให้ผู้ติดต่อของคุณเพิ่มการแสดงปฏิกิริยาต่อข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to call you." xml:space="preserve" approved="no"> + <trans-unit id="Allow your contacts to call you." xml:space="preserve"> <source>Allow your contacts to call you.</source> - <target state="translated">อนุญาตให้ผู้ติดต่อของคุณโทรหาคุณ</target> + <target>อนุญาตให้ผู้ติดต่อของคุณโทรหาคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve" approved="no"> + <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> <source>Allow your contacts to irreversibly delete sent messages.</source> - <target state="translated">อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร</target> + <target>อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> <source>Allow your contacts to send disappearing messages.</source> - <target state="translated">อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (disappearing messages)</target> + <target>อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (disappearing messages)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> <source>Allow your contacts to send voice messages.</source> - <target state="translated">อนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียง</target> + <target>อนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve" approved="no"> + <trans-unit id="Already connected?" xml:space="preserve"> <source>Already connected?</source> - <target state="translated">เชื่อมต่อสำเร็จแล้ว?</target> + <target>เชื่อมต่อสำเร็จแล้ว?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Always use relay" xml:space="preserve" approved="no"> + <trans-unit id="Always use relay" xml:space="preserve"> <source>Always use relay</source> - <target state="translated">ใช้รีเลย์เสมอ</target> + <target>ใช้รีเลย์เสมอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no"> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> - <target state="translated">โปรไฟล์แชทที่ว่างเปล่าพร้อมชื่อที่ให้ไว้ได้ถูกสร้างขึ้นและแอปจะเปิดตามปกติ</target> + <target>โปรไฟล์แชทที่ว่างเปล่าพร้อมชื่อที่ให้ไว้ได้ถูกสร้างขึ้นและแอปจะเปิดตามปกติ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Answer call" xml:space="preserve" approved="no"> + <trans-unit id="Answer call" xml:space="preserve"> <source>Answer call</source> - <target state="translated">รับสาย</target> + <target>รับสาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve" approved="no"> + <trans-unit id="App build: %@" xml:space="preserve"> <source>App build: %@</source> - <target state="translated">รุ่นแอป: %@</target> + <target>รุ่นแอป: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App icon" xml:space="preserve" approved="no"> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> - <target state="translated">ไอคอนแอป</target> + <target>ไอคอนแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App passcode" xml:space="preserve" approved="no"> + <trans-unit id="App passcode" xml:space="preserve"> <source>App passcode</source> - <target state="translated">รหัสผ่านแอป</target> + <target>รหัสผ่านแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve" approved="no"> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> <source>App passcode is replaced with self-destruct passcode.</source> - <target state="translated">รหัสผ่านแอปจะถูกแทนที่ด้วยรหัสผ่านที่ทำลายตัวเอง</target> + <target>รหัสผ่านแอปจะถูกแทนที่ด้วยรหัสผ่านที่ทำลายตัวเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version" xml:space="preserve" approved="no"> + <trans-unit id="App version" xml:space="preserve"> <source>App version</source> - <target state="translated">เวอร์ชันแอป</target> + <target>เวอร์ชันแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve" approved="no"> + <trans-unit id="App version: v%@" xml:space="preserve"> <source>App version: v%@</source> - <target state="translated">เวอร์ชันแอป: v%@</target> + <target>เวอร์ชันแอป: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Appearance" xml:space="preserve" approved="no"> + <trans-unit id="Appearance" xml:space="preserve"> <source>Appearance</source> - <target state="translated">รูปร่างลักษณะ</target> + <target>รูปร่างลักษณะ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Attach" xml:space="preserve" approved="no"> + <trans-unit id="Attach" xml:space="preserve"> <source>Attach</source> - <target state="translated">แนบ</target> + <target>แนบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve" approved="no"> + <trans-unit id="Audio & video calls" xml:space="preserve"> <source>Audio & video calls</source> - <target state="translated">การโทรด้วยเสียงและวิดีโอ</target> + <target>การโทรด้วยเสียงและวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio and video calls" xml:space="preserve" approved="no"> + <trans-unit id="Audio and video calls" xml:space="preserve"> <source>Audio and video calls</source> - <target state="translated">การโทรด้วยเสียงและวิดีโอ</target> + <target>การโทรด้วยเสียงและวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio/video calls" xml:space="preserve" approved="no"> + <trans-unit id="Audio/video calls" xml:space="preserve"> <source>Audio/video calls</source> - <target state="translated">การโทรด้วยเสียง/วิดีโอ</target> + <target>การโทรด้วยเสียง/วิดีโอ</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Audio/video calls are prohibited." xml:space="preserve" approved="no"> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> <source>Audio/video calls are prohibited.</source> - <target state="translated">การโทรด้วยเสียง/วิดีโอถูกห้าม</target> + <target>การโทรด้วยเสียง/วิดีโอถูกห้าม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication cancelled" xml:space="preserve" approved="no"> + <trans-unit id="Authentication cancelled" xml:space="preserve"> <source>Authentication cancelled</source> - <target state="translated">การยืนยันถูกยกเลิกแล้ว</target> + <target>การยืนยันถูกยกเลิกแล้ว</target> <note>PIN entry</note> </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve" approved="no"> + <trans-unit id="Authentication failed" xml:space="preserve"> <source>Authentication failed</source> - <target state="translated">การยืนยันล้มเหลว</target> + <target>การยืนยันล้มเหลว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve" approved="no"> + <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> <source>Authentication is required before the call is connected, but you may miss calls.</source> - <target state="translated">การยืนยันตัวตนเป็นที่จำเป็นก่อนเชื่อมต่อสาย แต่คุณอาจไม่ได้รับสาย</target> + <target>การยืนยันตัวตนเป็นที่จำเป็นก่อนเชื่อมต่อสาย แต่คุณอาจไม่ได้รับสาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve" approved="no"> + <trans-unit id="Authentication unavailable" xml:space="preserve"> <source>Authentication unavailable</source> - <target state="translated">การยืนยันไม่พร้อมใช้งาน</target> + <target>การยืนยันไม่พร้อมใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept" xml:space="preserve" approved="no"> + <trans-unit id="Auto-accept" xml:space="preserve"> <source>Auto-accept</source> - <target state="translated">ยอมรับอัตโนมัติ</target> + <target>ยอมรับอัตโนมัติ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve" approved="no"> + <trans-unit id="Auto-accept contact requests" xml:space="preserve"> <source>Auto-accept contact requests</source> - <target state="translated">ตอบรับคำขอเป็นเพื่อนโดยอัตโนมัติ</target> + <target>ตอบรับคำขอเป็นเพื่อนโดยอัตโนมัติ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Auto-accept images" xml:space="preserve" approved="no"> + <trans-unit id="Auto-accept images" xml:space="preserve"> <source>Auto-accept images</source> - <target state="translated">ยอมรับภาพอัตโนมัติ</target> + <target>ยอมรับภาพอัตโนมัติ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Back" xml:space="preserve" approved="no"> + <trans-unit id="Back" xml:space="preserve"> <source>Back</source> - <target state="translated">กลับ</target> + <target>กลับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Bad message ID" xml:space="preserve" approved="no"> + <trans-unit id="Bad message ID" xml:space="preserve"> <source>Bad message ID</source> - <target state="translated">ID ข้อความที่ไม่ดี</target> + <target>ID ข้อความที่ไม่ดี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Bad message hash" xml:space="preserve" approved="no"> + <trans-unit id="Bad message hash" xml:space="preserve"> <source>Bad message hash</source> - <target state="translated">แฮชข้อความไม่ดี</target> + <target>แฮชข้อความไม่ดี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Better messages" xml:space="preserve" approved="no"> + <trans-unit id="Better messages" xml:space="preserve"> <source>Better messages</source> - <target state="translated">ข้อความที่ดีขึ้น</target> + <target>ข้อความที่ดีขึ้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> <source>Both you and your contact can add message reactions.</source> - <target state="translated">ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</target> + <target>ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve" approved="no"> + <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> <source>Both you and your contact can irreversibly delete sent messages.</source> - <target state="translated">ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</target> + <target>ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can make calls." xml:space="preserve" approved="no"> + <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> <source>Both you and your contact can make calls.</source> - <target state="translated">ทั้งคุณและผู้ติดต่อของคุณสามารถโทรออกได้</target> + <target>ทั้งคุณและผู้ติดต่อของคุณสามารถโทรออกได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> <source>Both you and your contact can send disappearing messages.</source> - <target state="translated">ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความที่หายไปได้</target> + <target>ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความที่หายไปได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> <source>Both you and your contact can send voice messages.</source> - <target state="translated">ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้</target> + <target>ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve" approved="no"> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> - <target state="translated">ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า)</target> + <target>ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Call already ended!" xml:space="preserve" approved="no"> + <trans-unit id="Call already ended!" xml:space="preserve"> <source>Call already ended!</source> - <target state="translated">สิ้นสุดการโทรแล้ว!</target> + <target>สิ้นสุดการโทรแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Calls" xml:space="preserve" approved="no"> + <trans-unit id="Calls" xml:space="preserve"> <source>Calls</source> - <target state="translated">โทร</target> + <target>โทร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve" approved="no"> + <trans-unit id="Can't delete user profile!" xml:space="preserve"> <source>Can't delete user profile!</source> - <target state="translated">ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</target> + <target>ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve" approved="no"> + <trans-unit id="Can't invite contact!" xml:space="preserve"> <source>Can't invite contact!</source> - <target state="translated">ไม่สามารถเชิญผู้ติดต่อได้!</target> + <target>ไม่สามารถเชิญผู้ติดต่อได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve" approved="no"> + <trans-unit id="Can't invite contacts!" xml:space="preserve"> <source>Can't invite contacts!</source> - <target state="translated">ไม่สามารถเชิญผู้ติดต่อทั้งหลายได้!</target> + <target>ไม่สามารถเชิญผู้ติดต่อได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cancel" xml:space="preserve" approved="no"> + <trans-unit id="Cancel" xml:space="preserve"> <source>Cancel</source> - <target state="translated">ยกเลิก</target> + <target>ยกเลิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no"> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> <source>Cannot access keychain to save database password</source> - <target state="translated">ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล</target> + <target>ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve" approved="no"> + <trans-unit id="Cannot receive file" xml:space="preserve"> <source>Cannot receive file</source> - <target state="translated">ไม่สามารถรับไฟล์ได้</target> + <target>ไม่สามารถรับไฟล์ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change" xml:space="preserve" approved="no"> + <trans-unit id="Change" xml:space="preserve"> <source>Change</source> - <target state="translated">เปลี่ยน</target> + <target>เปลี่ยน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve" approved="no"> + <trans-unit id="Change database passphrase?" xml:space="preserve"> <source>Change database passphrase?</source> - <target state="translated">เปลี่ยนรหัสผ่านฐานข้อมูล?</target> + <target>เปลี่ยนรหัสผ่านฐานข้อมูล?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change lock mode" xml:space="preserve" approved="no"> + <trans-unit id="Change lock mode" xml:space="preserve"> <source>Change lock mode</source> - <target state="translated">เปลี่ยนโหมดล็อค</target> + <target>เปลี่ยนโหมดล็อค</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve" approved="no"> + <trans-unit id="Change member role?" xml:space="preserve"> <source>Change member role?</source> - <target state="translated">เปลี่ยนบทบาทของสมาชิก?</target> + <target>เปลี่ยนบทบาทของสมาชิก?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change passcode" xml:space="preserve" approved="no"> + <trans-unit id="Change passcode" xml:space="preserve"> <source>Change passcode</source> - <target state="translated">เปลี่ยนรหัสผ่าน</target> + <target>เปลี่ยนรหัสผ่าน</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve" approved="no"> + <trans-unit id="Change receiving address" xml:space="preserve"> <source>Change receiving address</source> - <target state="translated">เปลี่ยนที่อยู่ผู้รับ</target> + <target>เปลี่ยนที่อยู่ผู้รับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change receiving address?" xml:space="preserve" approved="no"> + <trans-unit id="Change receiving address?" xml:space="preserve"> <source>Change receiving address?</source> - <target state="translated">เปลี่ยนที่อยู่ผู้รับ?</target> + <target>เปลี่ยนที่อยู่ผู้รับ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change role" xml:space="preserve" approved="no"> + <trans-unit id="Change role" xml:space="preserve"> <source>Change role</source> - <target state="translated">เปลี่ยนบทบาท</target> + <target>เปลี่ยนบทบาท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Change self-destruct mode" xml:space="preserve" approved="no"> + <trans-unit id="Change self-destruct mode" xml:space="preserve"> <source>Change self-destruct mode</source> - <target state="translated">เปลี่ยนโหมดทําลายตัวเอง</target> + <target>เปลี่ยนโหมดทําลายตัวเอง</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Change self-destruct passcode" xml:space="preserve" approved="no"> + <trans-unit id="Change self-destruct passcode" xml:space="preserve"> <source>Change self-destruct passcode</source> - <target state="translated">เปลี่ยนรหัสผ่านแบบทำลายตัวเอง</target> + <target>เปลี่ยนรหัสผ่านแบบทำลายตัวเอง</target> <note>authentication reason set passcode view</note> </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve" approved="no"> + <trans-unit id="Chat archive" xml:space="preserve"> <source>Chat archive</source> - <target state="translated">ที่เก็บแชทเก่า</target> + <target>ที่เก็บแชทถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat console" xml:space="preserve" approved="no"> + <trans-unit id="Chat console" xml:space="preserve"> <source>Chat console</source> - <target state="translated">คอนโซลแชท</target> + <target>คอนโซลแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database" xml:space="preserve" approved="no"> + <trans-unit id="Chat database" xml:space="preserve"> <source>Chat database</source> - <target state="translated">ฐานข้อมูลแชท</target> + <target>ฐานข้อมูลแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve" approved="no"> + <trans-unit id="Chat database deleted" xml:space="preserve"> <source>Chat database deleted</source> - <target state="translated">ลบฐานข้อมูลแชทแล้ว</target> + <target>ลบฐานข้อมูลแชทแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve" approved="no"> + <trans-unit id="Chat database imported" xml:space="preserve"> <source>Chat database imported</source> - <target state="translated">นำฐานข้อมูลแชทเข้าแล้ว</target> + <target>นำฐานข้อมูลแชทเข้าแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve" approved="no"> + <trans-unit id="Chat is running" xml:space="preserve"> <source>Chat is running</source> - <target state="translated">แชทกําลังทํางานอยู่</target> + <target>แชทกำลังทำงานอยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat is stopped" xml:space="preserve" approved="no"> + <trans-unit id="Chat is stopped" xml:space="preserve"> <source>Chat is stopped</source> - <target state="translated">การแชทหยุดทํางานแล้ว</target> + <target>การแชทหยุดทํางานแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve" approved="no"> + <trans-unit id="Chat preferences" xml:space="preserve"> <source>Chat preferences</source> - <target state="translated">การตั้งค่าการแชท</target> + <target>ค่ากําหนดในการแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chats" xml:space="preserve" approved="no"> + <trans-unit id="Chats" xml:space="preserve"> <source>Chats</source> - <target state="translated">แชท</target> + <target>แชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve" approved="no"> + <trans-unit id="Check server address and try again." xml:space="preserve"> <source>Check server address and try again.</source> - <target state="translated">ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง</target> + <target>ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Chinese and Spanish interface" xml:space="preserve" approved="no"> + <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> <source>Chinese and Spanish interface</source> - <target state="translated">อินเทอร์เฟซภาษาจีนและสเปน</target> + <target>อินเทอร์เฟซภาษาจีนและสเปน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose file" xml:space="preserve" approved="no"> + <trans-unit id="Choose file" xml:space="preserve"> <source>Choose file</source> - <target state="translated">เลือกไฟล์</target> + <target>เลือกไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve" approved="no"> + <trans-unit id="Choose from library" xml:space="preserve"> <source>Choose from library</source> - <target state="translated">เลือกจากอัลบั้ม</target> + <target>เลือกจากอัลบั้ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear" xml:space="preserve" approved="no"> + <trans-unit id="Clear" xml:space="preserve"> <source>Clear</source> - <target state="translated">ลบ</target> + <target>ลบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve" approved="no"> + <trans-unit id="Clear conversation" xml:space="preserve"> <source>Clear conversation</source> - <target state="translated">ลบการสนทนา</target> + <target>ลบการสนทนา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve" approved="no"> + <trans-unit id="Clear conversation?" xml:space="preserve"> <source>Clear conversation?</source> - <target state="translated">ลบการสนทนา?</target> + <target>ลบการสนทนา?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve" approved="no"> + <trans-unit id="Clear verification" xml:space="preserve"> <source>Clear verification</source> - <target state="translated">ล้างการยืนยัน</target> + <target>ล้างการยืนยัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Colors" xml:space="preserve" approved="no"> + <trans-unit id="Colors" xml:space="preserve"> <source>Colors</source> - <target state="translated">สี</target> + <target>สี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Compare file" xml:space="preserve" approved="no"> + <trans-unit id="Compare file" xml:space="preserve"> <source>Compare file</source> - <target state="translated">เปรียบเทียบไฟล์</target> + <target>เปรียบเทียบไฟล์</target> <note>server test step</note> </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve" approved="no"> + <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> <source>Compare security codes with your contacts.</source> - <target state="translated">เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ</target> + <target>เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve" approved="no"> + <trans-unit id="Configure ICE servers" xml:space="preserve"> <source>Configure ICE servers</source> - <target state="translated">กำหนดค่าเซิร์ฟเวอร์ ICE</target> + <target>กำหนดค่าเซิร์ฟเวอร์ ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm" xml:space="preserve" approved="no"> + <trans-unit id="Confirm" xml:space="preserve"> <source>Confirm</source> - <target state="translated">ยืนยัน</target> + <target>ยืนยัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm Passcode" xml:space="preserve" approved="no"> + <trans-unit id="Confirm Passcode" xml:space="preserve"> <source>Confirm Passcode</source> - <target state="translated">ยืนยันรหัสผ่าน</target> + <target>ยืนยันรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm database upgrades" xml:space="preserve" approved="no"> + <trans-unit id="Confirm database upgrades" xml:space="preserve"> <source>Confirm database upgrades</source> - <target state="translated">ยืนยันการอัพเกรดฐานข้อมูล</target> + <target>ยืนยันการอัพเกรดฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve" approved="no"> + <trans-unit id="Confirm new passphrase…" xml:space="preserve"> <source>Confirm new passphrase…</source> - <target state="translated">ยืนยันรหัสผ่านใหม่…</target> + <target>ยืนยันรหัสผ่านใหม่…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Confirm password" xml:space="preserve" approved="no"> + <trans-unit id="Confirm password" xml:space="preserve"> <source>Confirm password</source> - <target state="translated">ยืนยันรหัสผ่าน</target> + <target>ยืนยันรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect" xml:space="preserve" approved="no"> + <trans-unit id="Connect" xml:space="preserve"> <source>Connect</source> - <target state="translated">เชื่อมต่อ</target> + <target>เชื่อมต่อ</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve" approved="no"> - <source>Connect via contact link?</source> - <target state="translated">เชื่อมต่อผ่านลิงค์ติดต่อ?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via group link?" xml:space="preserve" approved="no"> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via group link?" xml:space="preserve"> <source>Connect via group link?</source> - <target state="translated">เชื่อมต่อผ่านลิงค์กลุ่ม?</target> + <target>เชื่อมต่อผ่านลิงค์กลุ่ม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link" xml:space="preserve" approved="no"> + <trans-unit id="Connect via link" xml:space="preserve"> <source>Connect via link</source> - <target state="translated">เชื่อมต่อผ่านลิงก์</target> + <target>เชื่อมต่อผ่านลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve" approved="no"> + <trans-unit id="Connect via link / QR code" xml:space="preserve"> <source>Connect via link / QR code</source> - <target state="translated">เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด</target> + <target>เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve" approved="no"> - <source>Connect via one-time link?</source> - <target state="translated">เชื่อมต่อผ่านลิงค์แบบครั้งเดียว?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connecting server…" xml:space="preserve" approved="no"> + <trans-unit id="Connecting server…" xml:space="preserve"> <source>Connecting to server…</source> - <target state="translated">กำลังเชื่อมต่อกับเซิร์ฟเวอร์…</target> + <target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connecting server… (error: %@)" xml:space="preserve" approved="no"> + <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> <source>Connecting to server… (error: %@)</source> - <target state="translated">กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ผิดพลาด: %@)</target> + <target>กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection" xml:space="preserve" approved="no"> + <trans-unit id="Connection" xml:space="preserve"> <source>Connection</source> - <target state="translated">การเชื่อมต่อ</target> + <target>การเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection error" xml:space="preserve" approved="no"> + <trans-unit id="Connection error" xml:space="preserve"> <source>Connection error</source> - <target state="translated">การเชื่อมต่อล้มเหลว</target> + <target>การเชื่อมต่อผิดพลาด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection error (AUTH)" xml:space="preserve" approved="no"> + <trans-unit id="Connection error (AUTH)" xml:space="preserve"> <source>Connection error (AUTH)</source> - <target state="translated">การเชื่อมต่อล้มเหลว (AUTH)</target> + <target>การเชื่อมต่อผิดพลาด (AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve" approved="no"> - <source>Connection request</source> - <target state="translated">คำขอเชื่อมต่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request sent!" xml:space="preserve" approved="no"> + <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> - <target state="translated">ส่งคําขอเชื่อมต่อแล้ว!</target> + <target>ส่งคําขอเชื่อมต่อแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection timeout" xml:space="preserve" approved="no"> + <trans-unit id="Connection timeout" xml:space="preserve"> <source>Connection timeout</source> - <target state="translated">หมดเวลาการเชื่อมต่อ</target> + <target>หมดเวลาการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact allows" xml:space="preserve" approved="no"> + <trans-unit id="Contact allows" xml:space="preserve"> <source>Contact allows</source> - <target state="translated">ผู้ติดต่ออนุญาต</target> + <target>ผู้ติดต่ออนุญาต</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact already exists" xml:space="preserve" approved="no"> + <trans-unit id="Contact already exists" xml:space="preserve"> <source>Contact already exists</source> - <target state="translated">ผู้ติดต่อรายนี้มีอยู่แล้ว</target> + <target>ผู้ติดต่อรายนี้มีอยู่แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> + <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> <source>Contact and all messages will be deleted - this cannot be undone!</source> - <target state="translated">ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้!</target> + <target>ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact hidden:" xml:space="preserve" approved="no"> + <trans-unit id="Contact hidden:" xml:space="preserve"> <source>Contact hidden:</source> - <target state="translated">ซ่อนผู้ติดต่อ:</target> + <target>ผู้ติดต่อถูกซ่อน:</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is connected" xml:space="preserve" approved="no"> + <trans-unit id="Contact is connected" xml:space="preserve"> <source>Contact is connected</source> - <target state="translated">เชื่อมต่อกับผู้ติดต่อแล้ว</target> + <target>เชื่อมต่อกับผู้ติดต่อแล้ว</target> <note>notification</note> </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve" approved="no"> + <trans-unit id="Contact is not connected yet!" xml:space="preserve"> <source>Contact is not connected yet!</source> - <target state="translated">ผู้ติดต่อยังไม่ได้เชื่อมต่อ!</target> + <target>ผู้ติดต่อยังไม่ได้เชื่อมต่อ!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact name" xml:space="preserve" approved="no"> + <trans-unit id="Contact name" xml:space="preserve"> <source>Contact name</source> - <target state="translated">ชื่อผู้ติดต่อ</target> + <target>ชื่อผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contact preferences" xml:space="preserve" approved="no"> + <trans-unit id="Contact preferences" xml:space="preserve"> <source>Contact preferences</source> - <target state="translated">การตั้งค่าผู้ติดต่อ</target> + <target>การกําหนดลักษณะการติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve" approved="no"> + <trans-unit id="Contacts" xml:space="preserve"> + <source>Contacts</source> + <target>ติดต่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> <source>Contacts can mark messages for deletion; you will be able to view them.</source> - <target state="translated">ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้</target> + <target>ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Continue" xml:space="preserve" approved="no"> + <trans-unit id="Continue" xml:space="preserve"> <source>Continue</source> - <target state="translated">ดำเนินการต่อ</target> + <target>ดำเนินการต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Copy" xml:space="preserve" approved="no"> + <trans-unit id="Copy" xml:space="preserve"> <source>Copy</source> - <target state="translated">คัดลอก</target> + <target>คัดลอก</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Core version: v%@" xml:space="preserve" approved="no"> + <trans-unit id="Core version: v%@" xml:space="preserve"> <source>Core version: v%@</source> - <target state="translated">รุ่นหลัก: v%@</target> + <target>รุ่นหลัก: v%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create" xml:space="preserve" approved="no"> + <trans-unit id="Create" xml:space="preserve"> <source>Create</source> - <target state="translated">สร้าง</target> + <target>สร้าง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create SimpleX address" xml:space="preserve" approved="no"> + <trans-unit id="Create SimpleX address" xml:space="preserve"> <source>Create SimpleX address</source> - <target state="translated">สร้างที่อยู่ SimpleX</target> + <target>สร้างที่อยู่ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve" approved="no"> + <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> <source>Create an address to let people connect with you.</source> - <target state="translated">สร้างที่อยู่เพื่อให้ผู้คนติดต่อกับคุณ</target> + <target>สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create file" xml:space="preserve" approved="no"> + <trans-unit id="Create file" xml:space="preserve"> <source>Create file</source> - <target state="translated">สร้างไฟล์</target> + <target>สร้างไฟล์</target> <note>server test step</note> </trans-unit> - <trans-unit id="Create group link" xml:space="preserve" approved="no"> + <trans-unit id="Create group link" xml:space="preserve"> <source>Create group link</source> - <target state="translated">สร้างลิงค์กลุ่ม</target> + <target>สร้างลิงค์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create link" xml:space="preserve" approved="no"> + <trans-unit id="Create link" xml:space="preserve"> <source>Create link</source> - <target state="translated">สร้างลิงค์</target> + <target>สร้างลิงค์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve" approved="no"> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> - <target state="translated">สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target> + <target>สร้างลิงก์เชิญแบบใช้ครั้งเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create queue" xml:space="preserve" approved="no"> + <trans-unit id="Create queue" xml:space="preserve"> <source>Create queue</source> - <target state="translated">สร้างคิว</target> + <target>สร้างคิว</target> <note>server test step</note> </trans-unit> - <trans-unit id="Create secret group" xml:space="preserve" approved="no"> + <trans-unit id="Create secret group" xml:space="preserve"> <source>Create secret group</source> - <target state="translated">สร้างกลุ่มลับ</target> + <target>สร้างกลุ่มลับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create your profile" xml:space="preserve" approved="no"> + <trans-unit id="Create your profile" xml:space="preserve"> <source>Create your profile</source> - <target state="translated">สร้างโปรไฟล์ของคุณ</target> + <target>สร้างโปรไฟล์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve" approved="no"> + <trans-unit id="Created on %@" xml:space="preserve"> <source>Created on %@</source> - <target state="translated">สร้างเมื่อ %@</target> + <target>สร้างเมื่อ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Current Passcode" xml:space="preserve" approved="no"> + <trans-unit id="Current Passcode" xml:space="preserve"> <source>Current Passcode</source> - <target state="translated">รหัสผ่านปัจจุบัน</target> + <target>รหัสผ่านปัจจุบัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Current passphrase…" xml:space="preserve" approved="no"> + <trans-unit id="Current passphrase…" xml:space="preserve"> <source>Current passphrase…</source> - <target state="translated">รหัสผ่านปัจจุบัน…</target> + <target>รหัสผ่านปัจจุบัน…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve" approved="no"> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> <source>Currently maximum supported file size is %@.</source> - <target state="translated">ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@</target> + <target>ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Custom time" xml:space="preserve" approved="no"> + <trans-unit id="Custom time" xml:space="preserve"> <source>Custom time</source> - <target state="translated">เวลาที่กําหนดเอง</target> + <target>เวลาที่กําหนดเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Dark" xml:space="preserve" approved="no"> + <trans-unit id="Dark" xml:space="preserve"> <source>Dark</source> - <target state="translated">มืด</target> + <target>มืด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database ID" xml:space="preserve" approved="no"> + <trans-unit id="Database ID" xml:space="preserve"> <source>Database ID</source> - <target state="translated">ID ฐานข้อมูล</target> + <target>ID ฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database ID: %d" xml:space="preserve" approved="no"> + <trans-unit id="Database ID: %d" xml:space="preserve"> <source>Database ID: %d</source> - <target state="translated">ID ฐานข้อมูล: %d</target> + <target>ID ฐานข้อมูล: %d</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve" approved="no"> + <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> <source>Database IDs and Transport isolation option.</source> - <target state="translated">ID ฐานข้อมูลและตัวเลือกการแยกการส่งผ่าน</target> + <target>ID ฐานข้อมูลและตัวเลือกการแยกการส่งผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database downgrade" xml:space="preserve" approved="no"> + <trans-unit id="Database downgrade" xml:space="preserve"> <source>Database downgrade</source> - <target state="translated">ดาวน์เกรดฐานข้อมูล</target> + <target>ดาวน์เกรดฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encrypted!" xml:space="preserve" approved="no"> + <trans-unit id="Database encrypted!" xml:space="preserve"> <source>Database encrypted!</source> - <target state="translated">encrypt ฐานข้อมูลอย่างปลอดภัยแล้ว!</target> + <target>ฐานข้อมูลถูก encrypt แล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve" approved="no"> + <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> <source>Database encryption passphrase will be updated and stored in the keychain. </source> - <target state="translated">รหัสผ่านแบบ encrypt ที่ใช้ในการเข้าฐานข้อมูลจะได้รับการอัปเดตและจัดเก็บไว้ใน keychain + <target>รหัสผ่านแบบ encrypt ที่ใช้ในการเข้าฐานข้อมูลจะได้รับการอัปเดตและจัดเก็บไว้ใน keychain </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve" approved="no"> + <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> <source>Database encryption passphrase will be updated. </source> - <target state="translated">รหัส encryption ของฐานข้อมูลจะได้รับการอัปเดต + <target>รหัสผ่าน encryption ของฐานข้อมูลจะได้รับการอัปเดต </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database error" xml:space="preserve" approved="no"> + <trans-unit id="Database error" xml:space="preserve"> <source>Database error</source> - <target state="translated">ฐานข้อมูลผิดพลาด</target> + <target>ฐานข้อมูลผิดพลาด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve" approved="no"> + <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> <source>Database is encrypted using a random passphrase, you can change it.</source> - <target state="translated">ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม คุณสามารถเปลี่ยนได้</target> + <target>ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม คุณสามารถเปลี่ยนได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve" approved="no"> + <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> - <target state="translated">ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม โปรดเปลี่ยนก่อนส่งออก</target> + <target>ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม โปรดเปลี่ยนก่อนส่งออก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase" xml:space="preserve" approved="no"> + <trans-unit id="Database passphrase" xml:space="preserve"> <source>Database passphrase</source> - <target state="translated">รหัสผ่านของฐานข้อมูล</target> + <target>รหัสผ่านของฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase & export" xml:space="preserve" approved="no"> + <trans-unit id="Database passphrase & export" xml:space="preserve"> <source>Database passphrase & export</source> - <target state="translated">รหัสผ่านฐานข้อมูล & ส่งออก</target> + <target>รหัสผ่านฐานข้อมูล & ส่งออก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve" approved="no"> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> <source>Database passphrase is different from saved in the keychain.</source> - <target state="translated">รหัสผ่านของฐานข้อมูลแตกต่างจากที่บันทึกไว้ใน keychain</target> + <target>รหัสผ่านของฐานข้อมูลแตกต่างจากที่บันทึกไว้ใน keychain</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database passphrase is required to open chat." xml:space="preserve" approved="no"> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> <source>Database passphrase is required to open chat.</source> - <target state="translated">ต้องใช้รหัสผ่านของฐานข้อมูลในการเปิดแชท</target> + <target>ต้องใช้รหัสผ่านของฐานข้อมูลในการเปิดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database upgrade" xml:space="preserve" approved="no"> + <trans-unit id="Database upgrade" xml:space="preserve"> <source>Database upgrade</source> - <target state="translated">อัพเกรดฐานข้อมูล</target> + <target>อัพเกรดฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve" approved="no"> + <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> <source>Database will be encrypted and the passphrase stored in the keychain. </source> - <target state="translated">ฐานข้อมูลจะถูก encrypt และรหัสผ่านจะถูกจัดเก็บไว้ใน keychain + <target>ฐานข้อมูลจะถูก encrypt และรหัสผ่านจะถูกจัดเก็บไว้ใน keychain </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database will be encrypted. " xml:space="preserve" approved="no"> + <trans-unit id="Database will be encrypted. " xml:space="preserve"> <source>Database will be encrypted. </source> - <target state="translated">ฐานข้อมูลจะถูก encrypt + <target>ฐานข้อมูลจะถูก encrypt </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve" approved="no"> + <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> <source>Database will be migrated when the app restarts</source> - <target state="translated">ระบบจะย้ายฐานข้อมูลเมื่อแอปรีสตาร์ท</target> + <target>ระบบจะย้ายฐานข้อมูลเมื่อแอปรีสตาร์ท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Decentralized" xml:space="preserve" approved="no"> + <trans-unit id="Decentralized" xml:space="preserve"> <source>Decentralized</source> - <target state="translated">กระจายอำนาจ</target> + <target>กระจายอำนาจแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Decryption error" xml:space="preserve" approved="no"> + <trans-unit id="Decryption error" xml:space="preserve"> <source>Decryption error</source> - <target state="translated">ข้อผิดพลาดในการถอดรหัส</target> - <note>No comment provided by engineer.</note> + <target>ข้อผิดพลาดในการ decrypt</target> + <note>message decrypt error item</note> </trans-unit> - <trans-unit id="Delete" xml:space="preserve" approved="no"> + <trans-unit id="Delete" xml:space="preserve"> <source>Delete</source> - <target state="translated">ลบ</target> + <target>ลบ</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve" approved="no"> + <trans-unit id="Delete Contact" xml:space="preserve"> <source>Delete Contact</source> - <target state="translated">ลบผู้ติดต่อ</target> + <target>ลบผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete address" xml:space="preserve" approved="no"> + <trans-unit id="Delete address" xml:space="preserve"> <source>Delete address</source> - <target state="translated">ลบที่อยู่</target> + <target>ลบที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete address?" xml:space="preserve" approved="no"> + <trans-unit id="Delete address?" xml:space="preserve"> <source>Delete address?</source> - <target state="translated">ลบที่อยู่?</target> + <target>ลบที่อยู่?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete after" xml:space="preserve" approved="no"> + <trans-unit id="Delete after" xml:space="preserve"> <source>Delete after</source> - <target state="translated">ลบหลังจาก</target> + <target>ลบหลังจาก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete all files" xml:space="preserve" approved="no"> + <trans-unit id="Delete all files" xml:space="preserve"> <source>Delete all files</source> - <target state="translated">ลบไฟล์ทั้งหมด</target> + <target>ลบไฟล์ทั้งหมด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve" approved="no"> + <trans-unit id="Delete archive" xml:space="preserve"> <source>Delete archive</source> - <target state="translated">ลบที่เก็บถาวร</target> + <target>ลบที่เก็บถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve" approved="no"> + <trans-unit id="Delete chat archive?" xml:space="preserve"> <source>Delete chat archive?</source> - <target state="translated">ลบที่เก็บแชทถาวร?</target> + <target>ลบที่เก็บแชทถาวร?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat profile" xml:space="preserve" approved="no"> + <trans-unit id="Delete chat profile" xml:space="preserve"> <source>Delete chat profile</source> - <target state="translated">ลบโปรไฟล์แชท</target> + <target>ลบโปรไฟล์แชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete chat profile?" xml:space="preserve" approved="no"> + <trans-unit id="Delete chat profile?" xml:space="preserve"> <source>Delete chat profile?</source> - <target state="translated">ลบโปรไฟล์แชทไหม?</target> + <target>ลบโปรไฟล์แชทไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete connection" xml:space="preserve" approved="no"> + <trans-unit id="Delete connection" xml:space="preserve"> <source>Delete connection</source> - <target state="translated">ลบการเชื่อมต่อ</target> + <target>ลบการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact" xml:space="preserve" approved="no"> + <trans-unit id="Delete contact" xml:space="preserve"> <source>Delete contact</source> - <target state="translated">ลบผู้ติดต่อ</target> + <target>ลบผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete contact?" xml:space="preserve" approved="no"> + <trans-unit id="Delete contact?" xml:space="preserve"> <source>Delete contact?</source> - <target state="translated">ลบผู้ติดต่อ?</target> + <target>ลบผู้ติดต่อ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete database" xml:space="preserve" approved="no"> + <trans-unit id="Delete database" xml:space="preserve"> <source>Delete database</source> - <target state="translated">ลบฐานข้อมูล</target> + <target>ลบฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete file" xml:space="preserve" approved="no"> + <trans-unit id="Delete file" xml:space="preserve"> <source>Delete file</source> - <target state="translated">ลบไฟล์</target> + <target>ลบไฟล์</target> <note>server test step</note> </trans-unit> - <trans-unit id="Delete files and media?" xml:space="preserve" approved="no"> + <trans-unit id="Delete files and media?" xml:space="preserve"> <source>Delete files and media?</source> - <target state="translated">ลบไฟล์และสื่อ?</target> + <target>ลบไฟล์และสื่อ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete files for all chat profiles" xml:space="preserve" approved="no"> + <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> <source>Delete files for all chat profiles</source> - <target state="translated">ลบไฟล์สําหรับโปรไฟล์แชททั้งหมด</target> + <target>ลบไฟล์สําหรับโปรไฟล์แชททั้งหมด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete for everyone" xml:space="preserve" approved="no"> + <trans-unit id="Delete for everyone" xml:space="preserve"> <source>Delete for everyone</source> - <target state="translated">ลบสำหรับทุกคน</target> + <target>ลบสำหรับทุกคน</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Delete for me" xml:space="preserve" approved="no"> + <trans-unit id="Delete for me" xml:space="preserve"> <source>Delete for me</source> - <target state="translated">ลบให้ฉัน</target> + <target>ลบให้ฉัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete group" xml:space="preserve" approved="no"> + <trans-unit id="Delete group" xml:space="preserve"> <source>Delete group</source> - <target state="translated">ลบกลุ่ม</target> + <target>ลบกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete group?" xml:space="preserve" approved="no"> + <trans-unit id="Delete group?" xml:space="preserve"> <source>Delete group?</source> - <target state="translated">ลบกลุ่มไหม?</target> + <target>ลบกลุ่ม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete invitation" xml:space="preserve" approved="no"> + <trans-unit id="Delete invitation" xml:space="preserve"> <source>Delete invitation</source> - <target state="translated">ลบคำเชิญ</target> + <target>ลบคำเชิญ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete link" xml:space="preserve" approved="no"> + <trans-unit id="Delete link" xml:space="preserve"> <source>Delete link</source> - <target state="translated">ลบลิงค์</target> + <target>ลบลิงค์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete link?" xml:space="preserve" approved="no"> + <trans-unit id="Delete link?" xml:space="preserve"> <source>Delete link?</source> - <target state="translated">ลบลิงค์ ไหม?</target> + <target>ลบลิงค์ ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete member message?" xml:space="preserve" approved="no"> + <trans-unit id="Delete member message?" xml:space="preserve"> <source>Delete member message?</source> - <target state="translated">ลบข้อความสมาชิก?</target> + <target>ลบข้อความสมาชิก?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete message?" xml:space="preserve" approved="no"> + <trans-unit id="Delete message?" xml:space="preserve"> <source>Delete message?</source> - <target state="translated">ลบข้อความ?</target> + <target>ลบข้อความ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete messages" xml:space="preserve" approved="no"> + <trans-unit id="Delete messages" xml:space="preserve"> <source>Delete messages</source> - <target state="translated">ลบข้อความ</target> + <target>ลบข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete messages after" xml:space="preserve" approved="no"> + <trans-unit id="Delete messages after" xml:space="preserve"> <source>Delete messages after</source> - <target state="translated">ลบข้อความทีหลัง</target> + <target>ลบข้อความหลังจาก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete old database" xml:space="preserve" approved="no"> + <trans-unit id="Delete old database" xml:space="preserve"> <source>Delete old database</source> - <target state="translated">ลบฐานข้อมูลเก่า</target> + <target>ลบฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete old database?" xml:space="preserve" approved="no"> + <trans-unit id="Delete old database?" xml:space="preserve"> <source>Delete old database?</source> - <target state="translated">ลบฐานข้อมูลเก่า?</target> + <target>ลบฐานข้อมูลเก่า?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve" approved="no"> + <trans-unit id="Delete pending connection" xml:space="preserve"> <source>Delete pending connection</source> - <target state="translated">ลบการเชื่อมต่อที่รอดำเนินการ</target> + <target>ลบการเชื่อมต่อที่รอดำเนินการ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete pending connection?" xml:space="preserve" approved="no"> + <trans-unit id="Delete pending connection?" xml:space="preserve"> <source>Delete pending connection?</source> - <target state="translated">ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?</target> + <target>ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete profile" xml:space="preserve" approved="no"> + <trans-unit id="Delete profile" xml:space="preserve"> <source>Delete profile</source> - <target state="translated">ลบโปรไฟล์</target> + <target>ลบโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete queue" xml:space="preserve" approved="no"> + <trans-unit id="Delete queue" xml:space="preserve"> <source>Delete queue</source> - <target state="translated">ลบคิว</target> + <target>ลบคิว</target> <note>server test step</note> </trans-unit> - <trans-unit id="Delete user profile?" xml:space="preserve" approved="no"> + <trans-unit id="Delete user profile?" xml:space="preserve"> <source>Delete user profile?</source> - <target state="translated">ลบโปรไฟล์ผู้ใช้?</target> + <target>ลบโปรไฟล์ผู้ใช้?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Deleted at" xml:space="preserve" approved="no"> + <trans-unit id="Deleted at" xml:space="preserve"> <source>Deleted at</source> - <target state="translated">ลบที่</target> + <target>ลบที่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Deleted at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Deleted at: %@" xml:space="preserve"> <source>Deleted at: %@</source> - <target state="translated">ลบที่: %@</target> + <target>ลบที่: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Description" xml:space="preserve" approved="no"> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> + <source>Delivery receipts are disabled!</source> + <target>ใบตอบรับการจัดส่งถูกปิดใช้งาน!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts!" xml:space="preserve"> + <source>Delivery receipts!</source> + <target>ใบตอบรับการจัดส่ง!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Description" xml:space="preserve"> <source>Description</source> - <target state="translated">คำอธิบาย</target> + <target>คำอธิบาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Develop" xml:space="preserve" approved="no"> + <trans-unit id="Develop" xml:space="preserve"> <source>Develop</source> - <target state="translated">พัฒนา</target> + <target>พัฒนา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Developer tools" xml:space="preserve" approved="no"> + <trans-unit id="Developer tools" xml:space="preserve"> <source>Developer tools</source> - <target state="translated">เครื่องมือสำหรับนักพัฒนา</target> + <target>เครื่องมือสำหรับนักพัฒนา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Device" xml:space="preserve" approved="no"> + <trans-unit id="Device" xml:space="preserve"> <source>Device</source> - <target state="translated">อุปกรณ์</target> + <target>อุปกรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve" approved="no"> + <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> <source>Device authentication is disabled. Turning off SimpleX Lock.</source> - <target state="translated">การตรวจสอบสิทธิ์อุปกรณ์ถูกปิดใช้งาน กำลังปิด SimpleX Lock</target> + <target>การตรวจสอบสิทธิ์อุปกรณ์ถูกปิดใช้งาน กำลังปิด SimpleX Lock</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve" approved="no"> + <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> - <target state="translated">การตรวจสอบอุปกรณ์ไม่ได้ถูกเปิดใช้งาน คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่าได้ เมื่อคุณเปิดใช้งานการตรวจสอบอุปกรณ์แล้ว</target> + <target>การตรวจสอบอุปกรณ์ไม่ได้ถูกเปิดใช้งาน คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่าได้ เมื่อคุณเปิดใช้งานการตรวจสอบอุปกรณ์แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve" approved="no"> + <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> <source>Different names, avatars and transport isolation.</source> - <target state="translated">ชื่ออวตารและการแยกการขนส่งที่แตกต่างกัน</target> + <target>ชื่ออวตารและการแยกการขนส่งที่แตกต่างกัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Direct messages" xml:space="preserve" approved="no"> + <trans-unit id="Direct messages" xml:space="preserve"> <source>Direct messages</source> - <target state="translated">ข้อความส่วนตัว</target> + <target>ข้อความโดยตรง</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> + <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> <source>Direct messages between members are prohibited in this group.</source> - <target state="translated">ข้อความส่วนตัวระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</target> + <target>ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disable SimpleX Lock" xml:space="preserve" approved="no"> + <trans-unit id="Disable (keep overrides)" xml:space="preserve"> + <source>Disable (keep overrides)</source> + <target>ปิดใช้งาน (เก็บการแทนที่)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> <source>Disable SimpleX Lock</source> - <target state="translated">ปิดการใช้งาน SimpleX Lock</target> + <target>ปิดการใช้งาน SimpleX Lock</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Disappearing message" xml:space="preserve" approved="no"> - <source>Disappearing message</source> - <target state="translated">ข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target> + <trans-unit id="Disable for all" xml:space="preserve"> + <source>Disable for all</source> + <target>ปิดการใช้งานสำหรับทุกคน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages" xml:space="preserve" approved="no"> + <trans-unit id="Disappearing message" xml:space="preserve"> + <source>Disappearing message</source> + <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing messages" xml:space="preserve"> <source>Disappearing messages</source> - <target state="translated">ข้อความที่จะหายไปหลังปิดแชท (disappearing message)</target> + <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message)</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve" approved="no"> + <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> <source>Disappearing messages are prohibited in this chat.</source> - <target state="translated">ข้อความที่จะหายไปหลังปิดแชท (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้</target> + <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no"> + <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> <source>Disappearing messages are prohibited in this group.</source> - <target state="translated">ข้อความที่จะหายไปหลังปิดแชท (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้</target> + <target>ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappears at" xml:space="preserve" approved="no"> + <trans-unit id="Disappears at" xml:space="preserve"> <source>Disappears at</source> - <target state="translated">หายไปที่</target> + <target>หายไปที่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappears at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Disappears at: %@" xml:space="preserve"> <source>Disappears at: %@</source> - <target state="translated">หายไปที่: %@</target> + <target>หายไปที่: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Disconnect" xml:space="preserve" approved="no"> + <trans-unit id="Disconnect" xml:space="preserve"> <source>Disconnect</source> - <target state="translated">ตัดการเชื่อมต่อ</target> + <target>ตัดการเชื่อมต่อ</target> <note>server test step</note> </trans-unit> - <trans-unit id="Display name" xml:space="preserve" approved="no"> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> - <target state="translated">ชื่อที่แสดง</target> + <target>ชื่อที่แสดง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Display name:" xml:space="preserve" approved="no"> + <trans-unit id="Display name:" xml:space="preserve"> <source>Display name:</source> - <target state="translated">ชื่อที่แสดง:</target> + <target>ชื่อที่แสดง:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve" approved="no"> + <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> <source>Do NOT use SimpleX for emergency calls.</source> - <target state="translated">อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน</target> + <target>อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Do it later" xml:space="preserve" approved="no"> + <trans-unit id="Do it later" xml:space="preserve"> <source>Do it later</source> - <target state="translated">ทำในภายหลัง</target> + <target>ทำในภายหลัง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Don't create address" xml:space="preserve" approved="no"> + <trans-unit id="Don't create address" xml:space="preserve"> <source>Don't create address</source> - <target state="translated">อย่าสร้างที่อยู่</target> + <target>อย่าสร้างที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Don't show again" xml:space="preserve" approved="no"> + <trans-unit id="Don't enable" xml:space="preserve"> + <source>Don't enable</source> + <target>อย่าเปิดใช้งาน</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't show again" xml:space="preserve"> <source>Don't show again</source> - <target state="translated">ไม่ต้องแสดงอีก</target> + <target>ไม่ต้องแสดงอีก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Downgrade and open chat" xml:space="preserve" approved="no"> + <trans-unit id="Downgrade and open chat" xml:space="preserve"> <source>Downgrade and open chat</source> - <target state="translated">ปรับลดรุ่นและเปิดแชท</target> + <target>ปรับลดรุ่นและเปิดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Download file" xml:space="preserve" approved="no"> + <trans-unit id="Download file" xml:space="preserve"> <source>Download file</source> - <target state="translated">ดาวน์โหลดไฟล์</target> + <target>ดาวน์โหลดไฟล์</target> <note>server test step</note> </trans-unit> - <trans-unit id="Duplicate display name!" xml:space="preserve" approved="no"> + <trans-unit id="Duplicate display name!" xml:space="preserve"> <source>Duplicate display name!</source> - <target state="translated">ชื่อที่แสดงซ้ำ!</target> + <target>ชื่อที่แสดงซ้ำ!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Duration" xml:space="preserve" approved="no"> + <trans-unit id="Duration" xml:space="preserve"> <source>Duration</source> - <target state="translated">ระยะเวลา</target> + <target>ระยะเวลา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Edit" xml:space="preserve" approved="no"> + <trans-unit id="Edit" xml:space="preserve"> <source>Edit</source> - <target state="translated">แก้ไข</target> + <target>แก้ไข</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Edit group profile" xml:space="preserve" approved="no"> + <trans-unit id="Edit group profile" xml:space="preserve"> <source>Edit group profile</source> - <target state="translated">แก้ไขโปรไฟล์กลุ่ม</target> + <target>แก้ไขโปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable" xml:space="preserve" approved="no"> + <trans-unit id="Enable" xml:space="preserve"> <source>Enable</source> - <target state="translated">เปิดใช้งาน</target> + <target>เปิดใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable SimpleX Lock" xml:space="preserve" approved="no"> + <trans-unit id="Enable (keep overrides)" xml:space="preserve"> + <source>Enable (keep overrides)</source> + <target>เปิดใช้งาน (เก็บการแทนที่)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> <source>Enable SimpleX Lock</source> - <target state="translated">เปิดใช้งาน SimpleX Lock</target> + <target>เปิดใช้งาน SimpleX Lock</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Enable TCP keep-alive" xml:space="preserve" approved="no"> + <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> <source>Enable TCP keep-alive</source> - <target state="translated">เปิดใช้งาน TCP Keep-alive</target> + <target>เปิดใช้งาน TCP Keep-alive</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable automatic message deletion?" xml:space="preserve" approved="no"> + <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> <source>Enable automatic message deletion?</source> - <target state="translated">เปิดใช้งานการลบข้อความอัตโนมัติไหม?</target> + <target>เปิดใช้งานการลบข้อความอัตโนมัติ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable instant notifications?" xml:space="preserve" approved="no"> + <trans-unit id="Enable for all" xml:space="preserve"> + <source>Enable for all</source> + <target>เปิดใช้งานสําหรับทุกคน</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable instant notifications?" xml:space="preserve"> <source>Enable instant notifications?</source> - <target state="translated">เปิดใช้งานการแจ้งเตือนทันที?</target> + <target>เปิดใช้งานการแจ้งเตือนทันที?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable lock" xml:space="preserve" approved="no"> + <trans-unit id="Enable lock" xml:space="preserve"> <source>Enable lock</source> - <target state="translated">เปิดใช้งานการล็อค</target> + <target>เปิดใช้งานการล็อค</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable notifications" xml:space="preserve" approved="no"> + <trans-unit id="Enable notifications" xml:space="preserve"> <source>Enable notifications</source> - <target state="translated">เปิดใช้งานการแจ้งเตือน</target> + <target>เปิดใช้งานการแจ้งเตือน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable periodic notifications?" xml:space="preserve" approved="no"> + <trans-unit id="Enable periodic notifications?" xml:space="preserve"> <source>Enable periodic notifications?</source> - <target state="translated">เปิดใช้การแจ้งเตือนเป็นระยะๆ ไหม?</target> + <target>เปิดใช้การแจ้งเตือนเป็นระยะๆ ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable self-destruct" xml:space="preserve" approved="no"> + <trans-unit id="Enable self-destruct" xml:space="preserve"> <source>Enable self-destruct</source> - <target state="translated">เปิดใช้งานการทำลายตัวเอง</target> + <target>เปิดใช้งานการทำลายตัวเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable self-destruct passcode" xml:space="preserve" approved="no"> + <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> <source>Enable self-destruct passcode</source> - <target state="translated">เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง</target> + <target>เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง</target> <note>set passcode view</note> </trans-unit> - <trans-unit id="Encrypt" xml:space="preserve" approved="no"> + <trans-unit id="Encrypt" xml:space="preserve"> <source>Encrypt</source> - <target state="translated">Encrypt</target> + <target>Encrypt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypt database?" xml:space="preserve" approved="no"> + <trans-unit id="Encrypt database?" xml:space="preserve"> <source>Encrypt database?</source> - <target state="translated">Encrypt ฐานข้อมูล?</target> + <target>Encrypt ฐานข้อมูล?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypted database" xml:space="preserve" approved="no"> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> - <target state="translated">Encrypt ฐานข้อมูลเรียบร้อยแล้ว</target> + <target>Encrypt ฐานข้อมูลเรียบร้อยแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypted message or another event" xml:space="preserve" approved="no"> + <trans-unit id="Encrypted message or another event" xml:space="preserve"> <source>Encrypted message or another event</source> - <target state="translated">ข้อความที่ encrypt หรือเหตุการณ์อื่น</target> + <target>ข้อความที่ encrypt หรือเหตุการณ์อื่น</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: database error" xml:space="preserve" approved="no"> + <trans-unit id="Encrypted message: database error" xml:space="preserve"> <source>Encrypted message: database error</source> - <target state="translated">ข้อความที่ encrypt: ความผิดพลาดในฐานข้อมูล</target> + <target>ข้อความที่ encrypt: ความผิดพลาดในฐานข้อมูล</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: database migration error" xml:space="preserve" approved="no"> + <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> <source>Encrypted message: database migration error</source> - <target state="translated">ข้อความที่ encrypt: ข้อผิดพลาดในการย้ายฐานข้อมูล</target> + <target>ข้อความที่ encrypt: ข้อผิดพลาดในการย้ายฐานข้อมูล</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: keychain error" xml:space="preserve" approved="no"> + <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> <source>Encrypted message: keychain error</source> - <target state="translated">ข้อความที่ encrypt: ข้อผิดพลาดของ keychain</target> + <target>ข้อความที่ encrypt: ข้อผิดพลาดของ keychain</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: no passphrase" xml:space="preserve" approved="no"> + <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> <source>Encrypted message: no passphrase</source> - <target state="translated">ข้อความที่ encrypt: ไม่มีรหัสผ่าน</target> + <target>ข้อความที่ encrypt: ไม่มีรหัสผ่าน</target> <note>notification</note> </trans-unit> - <trans-unit id="Encrypted message: unexpected error" xml:space="preserve" approved="no"> + <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> <source>Encrypted message: unexpected error</source> - <target state="translated">ข้อความที่ encrypt: ข้อผิดพลาดที่ไม่คาดคิด</target> + <target>ข้อความที่ encrypt: ข้อผิดพลาดที่ไม่คาดคิด</target> <note>notification</note> </trans-unit> - <trans-unit id="Enter Passcode" xml:space="preserve" approved="no"> + <trans-unit id="Enter Passcode" xml:space="preserve"> <source>Enter Passcode</source> - <target state="translated">ใส่รหัสผ่าน</target> + <target>ใส่รหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter correct passphrase." xml:space="preserve" approved="no"> + <trans-unit id="Enter correct passphrase." xml:space="preserve"> <source>Enter correct passphrase.</source> - <target state="translated">ใส่รหัสผ่านที่ถูกต้อง</target> + <target>ใส่รหัสผ่านที่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter passphrase…" xml:space="preserve" approved="no"> + <trans-unit id="Enter passphrase…" xml:space="preserve"> <source>Enter passphrase…</source> - <target state="translated">ใส่รหัสผ่าน</target> + <target>ใส่รหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter password above to show!" xml:space="preserve" approved="no"> + <trans-unit id="Enter password above to show!" xml:space="preserve"> <source>Enter password above to show!</source> - <target state="translated">ใส่รหัสผ่านด้านบนเพื่อแสดง!</target> + <target>ใส่รหัสผ่านด้านบนเพื่อแสดง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter server manually" xml:space="preserve" approved="no"> + <trans-unit id="Enter server manually" xml:space="preserve"> <source>Enter server manually</source> - <target state="translated">ใส่เซิร์ฟเวอร์ด้วยตนเอง</target> + <target>ใส่เซิร์ฟเวอร์ด้วยตนเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enter welcome message…" xml:space="preserve" approved="no"> + <trans-unit id="Enter welcome message…" xml:space="preserve"> <source>Enter welcome message…</source> - <target state="translated">ใส่ข้อความต้อนรับ…</target> + <target>ใส่ข้อความต้อนรับ…</target> <note>placeholder</note> </trans-unit> - <trans-unit id="Enter welcome message… (optional)" xml:space="preserve" approved="no"> + <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> <source>Enter welcome message… (optional)</source> - <target state="translated">ใส่ข้อความต้อนรับ… (ไม่บังคับ)</target> + <target>ใส่ข้อความต้อนรับ… (ไม่บังคับ)</target> <note>placeholder</note> </trans-unit> - <trans-unit id="Error" xml:space="preserve" approved="no"> + <trans-unit id="Error" xml:space="preserve"> <source>Error</source> - <target state="translated">ผิดพลาด</target> + <target>ผิดพลาด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accepting contact request" xml:space="preserve" approved="no"> + <trans-unit id="Error aborting address change" xml:space="preserve"> + <source>Error aborting address change</source> + <target>เกิดข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error accepting contact request" xml:space="preserve"> <source>Error accepting contact request</source> - <target state="translated">เกิดข้อผิดพลาดในการรับคำขอติดต่อ</target> + <target>เกิดข้อผิดพลาดในการรับคำขอติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve" approved="no"> + <trans-unit id="Error accessing database file" xml:space="preserve"> <source>Error accessing database file</source> - <target state="translated">เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล</target> + <target>เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error adding member(s)" xml:space="preserve" approved="no"> + <trans-unit id="Error adding member(s)" xml:space="preserve"> <source>Error adding member(s)</source> - <target state="translated">เกิดข้อผิดพลาดในการเพิ่มสมาชิก</target> + <target>เกิดข้อผิดพลาดในการเพิ่มสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error changing address" xml:space="preserve" approved="no"> + <trans-unit id="Error changing address" xml:space="preserve"> <source>Error changing address</source> - <target state="translated">เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target> + <target>เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error changing role" xml:space="preserve" approved="no"> + <trans-unit id="Error changing role" xml:space="preserve"> <source>Error changing role</source> - <target state="translated">เกิดข้อผิดพลาดในการเปลี่ยนบทบาท</target> + <target>เกิดข้อผิดพลาดในการเปลี่ยนบทบาท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error changing setting" xml:space="preserve" approved="no"> + <trans-unit id="Error changing setting" xml:space="preserve"> <source>Error changing setting</source> - <target state="translated">เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</target> + <target>เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error creating address" xml:space="preserve" approved="no"> + <trans-unit id="Error creating address" xml:space="preserve"> <source>Error creating address</source> - <target state="translated">เกิดข้อผิดพลาดในการสร้างที่อยู่</target> + <target>เกิดข้อผิดพลาดในการสร้างที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error creating group" xml:space="preserve" approved="no"> + <trans-unit id="Error creating group" xml:space="preserve"> <source>Error creating group</source> - <target state="translated">เกิดข้อผิดพลาดในการสร้างกลุ่ม</target> + <target>เกิดข้อผิดพลาดในการสร้างกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error creating group link" xml:space="preserve" approved="no"> + <trans-unit id="Error creating group link" xml:space="preserve"> <source>Error creating group link</source> - <target state="translated">เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม</target> + <target>เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error creating profile!" xml:space="preserve" approved="no"> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> - <target state="translated">เกิดข้อผิดพลาดในการสร้างโปรไฟล์!</target> + <target>เกิดข้อผิดพลาดในการสร้างโปรไฟล์!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting chat database" xml:space="preserve" approved="no"> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> - <target state="translated">เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท</target> + <target>เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting chat!" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting chat!" xml:space="preserve"> <source>Error deleting chat!</source> - <target state="translated">เกิดข้อผิดพลาดในการลบแชท!</target> + <target>เกิดข้อผิดพลาดในการลบแชท!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting connection" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting connection" xml:space="preserve"> <source>Error deleting connection</source> - <target state="translated">เกิดข้อผิดพลาดในการลบการเชื่อมต่อ</target> + <target>เกิดข้อผิดพลาดในการลบการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting contact" xml:space="preserve"> <source>Error deleting contact</source> - <target state="translated">เกิดข้อผิดพลาดในการลบผู้ติดต่อ</target> + <target>เกิดข้อผิดพลาดในการลบผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting database" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting database" xml:space="preserve"> <source>Error deleting database</source> - <target state="translated">เกิดข้อผิดพลาดในการลบฐานข้อมูล</target> + <target>เกิดข้อผิดพลาดในการลบฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting old database" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting old database" xml:space="preserve"> <source>Error deleting old database</source> - <target state="translated">เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า</target> + <target>เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting token" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting token" xml:space="preserve"> <source>Error deleting token</source> - <target state="translated">เกิดข้อผิดพลาดในการลบโทเค็น</target> + <target>เกิดข้อผิดพลาดในการลบโทเค็น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error deleting user profile" xml:space="preserve" approved="no"> + <trans-unit id="Error deleting user profile" xml:space="preserve"> <source>Error deleting user profile</source> - <target state="translated">เกิดข้อผิดพลาดในการลบโปรไฟล์ผู้ใช้</target> + <target>เกิดข้อผิดพลาดในการลบโปรไฟล์ผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error enabling notifications" xml:space="preserve" approved="no"> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> + <source>Error enabling delivery receipts!</source> + <target>เกิดข้อผิดพลาดในการเปิดใช้ใบเสร็จการจัดส่ง!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling notifications" xml:space="preserve"> <source>Error enabling notifications</source> - <target state="translated">เกิดข้อผิดพลาดในการเปิดใช้งานการแจ้งเตือน</target> + <target>เกิดข้อผิดพลาดในการเปิดใช้งานการแจ้งเตือน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error encrypting database" xml:space="preserve" approved="no"> + <trans-unit id="Error encrypting database" xml:space="preserve"> <source>Error encrypting database</source> - <target state="translated">เกิดข้อผิดพลาดในการ encrypt ฐานข้อมูล</target> + <target>เกิดข้อผิดพลาดในการ encrypt ฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error exporting chat database" xml:space="preserve" approved="no"> + <trans-unit id="Error exporting chat database" xml:space="preserve"> <source>Error exporting chat database</source> - <target state="translated">เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท</target> + <target>เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error importing chat database" xml:space="preserve" approved="no"> + <trans-unit id="Error importing chat database" xml:space="preserve"> <source>Error importing chat database</source> - <target state="translated">เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท</target> + <target>เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error joining group" xml:space="preserve" approved="no"> + <trans-unit id="Error joining group" xml:space="preserve"> <source>Error joining group</source> - <target state="translated">เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม</target> + <target>เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve" approved="no"> + <trans-unit id="Error loading %@ servers" xml:space="preserve"> <source>Error loading %@ servers</source> - <target state="translated">โหลดเซิร์ฟเวอร์ %@ ผิดพลาด</target> + <target>โหลดเซิร์ฟเวอร์ %@ ผิดพลาด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error receiving file" xml:space="preserve" approved="no"> + <trans-unit id="Error receiving file" xml:space="preserve"> <source>Error receiving file</source> - <target state="translated">เกิดข้อผิดพลาดในการรับไฟล์</target> + <target>เกิดข้อผิดพลาดในการรับไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error removing member" xml:space="preserve" approved="no"> + <trans-unit id="Error removing member" xml:space="preserve"> <source>Error removing member</source> - <target state="translated">เกิดข้อผิดพลาดในการลบสมาชิก</target> + <target>เกิดข้อผิดพลาดในการลบสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve" approved="no"> + <trans-unit id="Error saving %@ servers" xml:space="preserve"> <source>Error saving %@ servers</source> - <target state="translated">เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@</target> + <target>เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving ICE servers" xml:space="preserve" approved="no"> + <trans-unit id="Error saving ICE servers" xml:space="preserve"> <source>Error saving ICE servers</source> - <target state="translated">เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE</target> + <target>เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving group profile" xml:space="preserve" approved="no"> + <trans-unit id="Error saving group profile" xml:space="preserve"> <source>Error saving group profile</source> - <target state="translated">เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม</target> + <target>เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving passcode" xml:space="preserve" approved="no"> + <trans-unit id="Error saving passcode" xml:space="preserve"> <source>Error saving passcode</source> - <target state="translated">เกิดข้อผิดพลาดในการบันทึกรหัสผ่าน</target> + <target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving passphrase to keychain" xml:space="preserve" approved="no"> + <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> <source>Error saving passphrase to keychain</source> - <target state="translated">เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain</target> + <target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving user password" xml:space="preserve" approved="no"> + <trans-unit id="Error saving user password" xml:space="preserve"> <source>Error saving user password</source> - <target state="translated">เกิดข้อผิดพลาดในการบันทึกรหัสผ่านของผู้ใช้</target> + <target>เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error sending email" xml:space="preserve" approved="no"> + <trans-unit id="Error sending email" xml:space="preserve"> <source>Error sending email</source> - <target state="translated">เกิดข้อผิดพลาดในการส่งอีเมล</target> + <target>เกิดข้อผิดพลาดในการส่งอีเมล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error sending message" xml:space="preserve" approved="no"> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> - <target state="translated">เกิดข้อผิดพลาดในการส่งข้อความ</target> + <target>เกิดข้อผิดพลาดในการส่งข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error starting chat" xml:space="preserve" approved="no"> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> + <source>Error setting delivery receipts!</source> + <target>เกิดข้อผิดพลาดในการตั้งค่าใบตอบรับการจัดส่ง!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error starting chat" xml:space="preserve"> <source>Error starting chat</source> - <target state="translated">เกิดข้อผิดพลาดในการเริ่มแชท</target> + <target>เกิดข้อผิดพลาดในการเริ่มแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error stopping chat" xml:space="preserve" approved="no"> + <trans-unit id="Error stopping chat" xml:space="preserve"> <source>Error stopping chat</source> - <target state="translated">เกิดข้อผิดพลาดในการหยุดแชท</target> + <target>เกิดข้อผิดพลาดในการหยุดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error switching profile!" xml:space="preserve" approved="no"> + <trans-unit id="Error switching profile!" xml:space="preserve"> <source>Error switching profile!</source> - <target state="translated">เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!</target> + <target>เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error updating group link" xml:space="preserve" approved="no"> + <trans-unit id="Error synchronizing connection" xml:space="preserve"> + <source>Error synchronizing connection</source> + <target>เกิดข้อผิดพลาดในการซิงโครไนซ์การเชื่อมต่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating group link" xml:space="preserve"> <source>Error updating group link</source> - <target state="translated">เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม</target> + <target>เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error updating message" xml:space="preserve" approved="no"> + <trans-unit id="Error updating message" xml:space="preserve"> <source>Error updating message</source> - <target state="translated">เกิดข้อผิดพลาดในการอัปเดตข้อความ</target> + <target>เกิดข้อผิดพลาดในการอัปเดตข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error updating settings" xml:space="preserve" approved="no"> + <trans-unit id="Error updating settings" xml:space="preserve"> <source>Error updating settings</source> - <target state="translated">เกิดข้อผิดพลาดในการอัปเดตการตั้งค่า</target> + <target>เกิดข้อผิดพลาดในการอัปเดตการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error updating user privacy" xml:space="preserve" approved="no"> + <trans-unit id="Error updating user privacy" xml:space="preserve"> <source>Error updating user privacy</source> - <target state="translated">เกิดข้อผิดพลาดในการอัปเดตข้อมูลส่วนตัวของผู้ใช้</target> + <target>เกิดข้อผิดพลาดในการอัปเดตข้อมูลส่วนตัวของผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error: " xml:space="preserve" approved="no"> + <trans-unit id="Error: " xml:space="preserve"> <source>Error: </source> - <target state="translated">ผิดพลาด: </target> + <target>ผิดพลาด: </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error: %@" xml:space="preserve" approved="no"> + <trans-unit id="Error: %@" xml:space="preserve"> <source>Error: %@</source> - <target state="translated">ข้อผิดพลาด: % @</target> + <target>ข้อผิดพลาด: % @</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error: URL is invalid" xml:space="preserve" approved="no"> + <trans-unit id="Error: URL is invalid" xml:space="preserve"> <source>Error: URL is invalid</source> - <target state="translated">เกิดข้อผิดพลาด: URL ไม่ถูกต้อง</target> + <target>เกิดข้อผิดพลาด: URL ไม่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error: no database file" xml:space="preserve" approved="no"> + <trans-unit id="Error: no database file" xml:space="preserve"> <source>Error: no database file</source> - <target state="translated">เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล</target> + <target>เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Exit without saving" xml:space="preserve" approved="no"> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> + <source>Even when disabled in the conversation.</source> + <target>แม้ในขณะที่ปิดใช้งานในการสนทนา</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exit without saving" xml:space="preserve"> <source>Exit without saving</source> - <target state="translated">ออกโดยไม่บันทึก</target> + <target>ออกโดยไม่บันทึก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Export database" xml:space="preserve" approved="no"> + <trans-unit id="Export database" xml:space="preserve"> <source>Export database</source> - <target state="translated">ส่งออกฐานข้อมูล</target> + <target>ส่งออกฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Export error:" xml:space="preserve" approved="no"> + <trans-unit id="Export error:" xml:space="preserve"> <source>Export error:</source> - <target state="translated">ข้อผิดพลาดในการส่งออก:</target> + <target>ข้อผิดพลาดในการส่งออก:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Exported database archive." xml:space="preserve" approved="no"> + <trans-unit id="Exported database archive." xml:space="preserve"> <source>Exported database archive.</source> - <target state="translated">ที่เก็บถาวรฐานข้อมูลที่ส่งออก</target> + <target>ที่เก็บถาวรฐานข้อมูลที่ส่งออก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve" approved="no"> - <source>Exporting database archive...</source> - <target state="translated">กำลังส่งออกที่เก็บถาวรฐานข้อมูล...</target> + <trans-unit id="Exporting database archive…" xml:space="preserve"> + <source>Exporting database archive…</source> + <target>กำลังส่งออกที่เก็บถาวรฐานข้อมูล…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Failed to remove passphrase" xml:space="preserve" approved="no"> + <trans-unit id="Failed to remove passphrase" xml:space="preserve"> <source>Failed to remove passphrase</source> - <target state="translated">ไม่สามารถลบรหัสผ่านได้</target> + <target>ไม่สามารถลบรหัสผ่านได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve" approved="no"> + <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> <source>Fast and no wait until the sender is online!</source> - <target state="translated">รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</target> + <target>รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="File will be deleted from servers." xml:space="preserve" approved="no"> + <trans-unit id="Favorite" xml:space="preserve"> + <source>Favorite</source> + <target>ที่ชอบ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be deleted from servers." xml:space="preserve"> <source>File will be deleted from servers.</source> - <target state="translated">ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์</target> + <target>ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve" approved="no"> + <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> <source>File will be received when your contact completes uploading it.</source> - <target state="translated">จะได้รับไฟล์เมื่อผู้ติดต่อของคุณอัปโหลดเสร็จ</target> + <target>จะได้รับไฟล์เมื่อผู้ติดต่อของคุณอัปโหลดเสร็จ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve" approved="no"> + <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>File will be received when your contact is online, please wait or check later!</source> - <target state="translated">จะได้รับไฟล์เมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</target> + <target>จะได้รับไฟล์เมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="File: %@" xml:space="preserve" approved="no"> + <trans-unit id="File: %@" xml:space="preserve"> <source>File: %@</source> - <target state="translated">ไฟล์: % @</target> + <target>ไฟล์: % @</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Files & media" xml:space="preserve" approved="no"> + <trans-unit id="Files & media" xml:space="preserve"> <source>Files & media</source> - <target state="translated">ไฟล์และสื่อ</target> + <target>ไฟล์และสื่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Finally, we have them! 🚀" xml:space="preserve" approved="no"> + <trans-unit id="Files and media" xml:space="preserve"> + <source>Files and media</source> + <target>ไฟล์และสื่อ</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> + <source>Files and media are prohibited in this group.</source> + <target>ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media prohibited!" xml:space="preserve"> + <source>Files and media prohibited!</source> + <target>ไฟล์และสื่อต้องห้าม!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> + <source>Filter unread and favorite chats.</source> + <target>กรองแชทที่ยังไม่อ่านและแชทโปรด</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> <source>Finally, we have them! 🚀</source> - <target state="translated">ในที่สุดเราก็มีแล้ว! 🚀</target> + <target>ในที่สุดเราก็มีแล้ว! 🚀</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="For console" xml:space="preserve" approved="no"> + <trans-unit id="Find chats faster" xml:space="preserve"> + <source>Find chats faster</source> + <target>ค้นหาแชทได้เร็วขึ้น</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix" xml:space="preserve"> + <source>Fix</source> + <target>แก้ไข</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection" xml:space="preserve"> + <source>Fix connection</source> + <target>แก้ไขการเชื่อมต่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection?" xml:space="preserve"> + <source>Fix connection?</source> + <target>แก้ไขการเชื่อมต่อ?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> + <source>Fix encryption after restoring backups.</source> + <target>แก้ไข encryption หลังจากกู้คืนข้อมูลสำรอง</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by contact" xml:space="preserve"> + <source>Fix not supported by contact</source> + <target>การแก้ไขไม่รองรับโดยผู้ติดต่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by group member" xml:space="preserve"> + <source>Fix not supported by group member</source> + <target>การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For console" xml:space="preserve"> <source>For console</source> - <target state="translated">สำหรับคอนโซล</target> + <target>สำหรับคอนโซล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="French interface" xml:space="preserve" approved="no"> + <trans-unit id="French interface" xml:space="preserve"> <source>French interface</source> - <target state="translated">อินเทอร์เฟซภาษาฝรั่งเศส</target> + <target>อินเทอร์เฟซภาษาฝรั่งเศส</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full link" xml:space="preserve" approved="no"> + <trans-unit id="Full link" xml:space="preserve"> <source>Full link</source> - <target state="translated">ลิงค์เต็ม</target> + <target>ลิงค์เต็ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name (optional)" xml:space="preserve" approved="no"> + <trans-unit id="Full name (optional)" xml:space="preserve"> <source>Full name (optional)</source> - <target state="translated">ชื่อเต็ม (ไม่บังคับ)</target> + <target>ชื่อเต็ม (ไม่บังคับ)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Full name:" xml:space="preserve" approved="no"> + <trans-unit id="Full name:" xml:space="preserve"> <source>Full name:</source> - <target state="translated">ชื่อเต็ม:</target> + <target>ชื่อเต็ม:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve" approved="no"> + <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> <source>Fully re-implemented - work in background!</source> - <target state="translated">ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!</target> + <target>ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Further reduced battery usage" xml:space="preserve" approved="no"> + <trans-unit id="Further reduced battery usage" xml:space="preserve"> <source>Further reduced battery usage</source> - <target state="translated">ลดการใช้แบตเตอรี่เพิ่มเติม</target> + <target>ลดการใช้แบตเตอรี่เพิ่มเติม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="GIFs and stickers" xml:space="preserve" approved="no"> + <trans-unit id="GIFs and stickers" xml:space="preserve"> <source>GIFs and stickers</source> - <target state="translated">GIFs และสติกเกอร์</target> + <target>GIFs และสติกเกอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group" xml:space="preserve" approved="no"> + <trans-unit id="Group" xml:space="preserve"> <source>Group</source> - <target state="translated">กลุ่ม</target> + <target>กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group display name" xml:space="preserve" approved="no"> + <trans-unit id="Group display name" xml:space="preserve"> <source>Group display name</source> - <target state="translated">ชื่อกลุ่มที่แสดง</target> + <target>ชื่อกลุ่มที่แสดง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group full name (optional)" xml:space="preserve" approved="no"> + <trans-unit id="Group full name (optional)" xml:space="preserve"> <source>Group full name (optional)</source> - <target state="translated">ชื่อเต็มกลุ่ม (ไม่บังคับ)</target> + <target>ชื่อเต็มกลุ่ม (ไม่บังคับ)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group image" xml:space="preserve" approved="no"> + <trans-unit id="Group image" xml:space="preserve"> <source>Group image</source> - <target state="translated">ภาพกลุ่ม</target> + <target>ภาพกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group invitation" xml:space="preserve" approved="no"> + <trans-unit id="Group invitation" xml:space="preserve"> <source>Group invitation</source> - <target state="translated">คําเชิญเข้าร่วมกลุ่ม</target> + <target>คําเชิญเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group invitation expired" xml:space="preserve" approved="no"> + <trans-unit id="Group invitation expired" xml:space="preserve"> <source>Group invitation expired</source> - <target state="translated">คำเชิญเข้าร่วมกลุ่มหมดอายุแล้ว</target> + <target>คำเชิญเข้าร่วมกลุ่มหมดอายุแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve" approved="no"> + <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> <source>Group invitation is no longer valid, it was removed by sender.</source> - <target state="translated">คำเชิญเข้าร่วมกลุ่มใช้ไม่ได้อีกต่อไป คำเชิญถูกลบโดยผู้ส่ง</target> + <target>คำเชิญเข้าร่วมกลุ่มใช้ไม่ถูกต้องอีกต่อไป คำเชิญถูกลบโดยผู้ส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group link" xml:space="preserve" approved="no"> + <trans-unit id="Group link" xml:space="preserve"> <source>Group link</source> - <target state="translated">ลิงค์กลุ่ม</target> + <target>ลิงค์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group links" xml:space="preserve" approved="no"> + <trans-unit id="Group links" xml:space="preserve"> <source>Group links</source> - <target state="translated">ลิงค์กลุ่ม</target> + <target>ลิงค์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Group members can add message reactions." xml:space="preserve"> <source>Group members can add message reactions.</source> - <target state="translated">สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</target> + <target>สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve" approved="no"> + <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> <source>Group members can irreversibly delete sent messages.</source> - <target state="translated">สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</target> + <target>สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no"> + <trans-unit id="Group members can send direct messages." xml:space="preserve"> <source>Group members can send direct messages.</source> - <target state="translated">สมาชิกกลุ่มสามารถส่งข้อความส่วนตัวได้</target> + <target>สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> <source>Group members can send disappearing messages.</source> - <target state="translated">สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> + <target>สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Group members can send files and media." xml:space="preserve"> + <source>Group members can send files and media.</source> + <target>สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can send voice messages." xml:space="preserve"> <source>Group members can send voice messages.</source> - <target state="translated">สมาชิกกลุ่มสามารถส่งข้อความเสียง</target> + <target>สมาชิกกลุ่มสามารถส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group message:" xml:space="preserve" approved="no"> + <trans-unit id="Group message:" xml:space="preserve"> <source>Group message:</source> - <target state="translated">ข้อความกลุ่ม:</target> + <target>ข้อความกลุ่ม:</target> <note>notification</note> </trans-unit> - <trans-unit id="Group moderation" xml:space="preserve" approved="no"> + <trans-unit id="Group moderation" xml:space="preserve"> <source>Group moderation</source> - <target state="translated">การดูแลกลุ่ม</target> + <target>การกลั่นกรองกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group preferences" xml:space="preserve" approved="no"> + <trans-unit id="Group preferences" xml:space="preserve"> <source>Group preferences</source> - <target state="translated">การตั้งค่ากลุ่ม</target> + <target>ค่ากําหนดลักษณะกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group profile" xml:space="preserve" approved="no"> + <trans-unit id="Group profile" xml:space="preserve"> <source>Group profile</source> - <target state="translated">โปรไฟล์กลุ่ม</target> + <target>โปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve" approved="no"> + <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> <source>Group profile is stored on members' devices, not on the servers.</source> - <target state="translated">โปรไฟล์กลุ่มถูกจัดเก็บไว้ในอุปกรณ์ของสมาชิก ไม่ใช่บนเซิร์ฟเวอร์</target> + <target>โปรไฟล์กลุ่มถูกจัดเก็บไว้ในอุปกรณ์ของสมาชิก ไม่ใช่บนเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group welcome message" xml:space="preserve" approved="no"> + <trans-unit id="Group welcome message" xml:space="preserve"> <source>Group welcome message</source> - <target state="translated">ข้อความต้อนรับกลุ่ม</target> + <target>ข้อความต้อนรับกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve" approved="no"> + <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for all members - this cannot be undone!</source> - <target state="translated">กลุ่มจะถูกลบสำหรับสมาชิกทั้งหมด - ไม่สามารถยกเลิกได้!</target> + <target>กลุ่มจะถูกลบสำหรับสมาชิกทั้งหมด - ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve" approved="no"> + <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> <source>Group will be deleted for you - this cannot be undone!</source> - <target state="translated">กลุ่มจะถูกลบสำหรับคุณ - ไม่สามารถยกเลิกได้!</target> + <target>กลุ่มจะถูกลบสำหรับคุณ - ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Help" xml:space="preserve" approved="no"> + <trans-unit id="Help" xml:space="preserve"> <source>Help</source> - <target state="translated">ความช่วยเหลือ</target> + <target>ความช่วยเหลือ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hidden" xml:space="preserve" approved="no"> + <trans-unit id="Hidden" xml:space="preserve"> <source>Hidden</source> - <target state="translated">ซ่อนอยู่</target> + <target>ซ่อนอยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hidden chat profiles" xml:space="preserve" approved="no"> + <trans-unit id="Hidden chat profiles" xml:space="preserve"> <source>Hidden chat profiles</source> - <target state="translated">โปรไฟล์การแชทที่ซ่อนอยู่</target> + <target>โปรไฟล์การแชทที่ซ่อนอยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hidden profile password" xml:space="preserve" approved="no"> + <trans-unit id="Hidden profile password" xml:space="preserve"> <source>Hidden profile password</source> - <target state="translated">รหัสผ่านโปรไฟล์ที่ซ่อนอยู่</target> + <target>รหัสผ่านโปรไฟล์ที่ซ่อนอยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hide" xml:space="preserve" approved="no"> + <trans-unit id="Hide" xml:space="preserve"> <source>Hide</source> - <target state="translated">ซ่อน</target> + <target>ซ่อน</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Hide app screen in the recent apps." xml:space="preserve" approved="no"> + <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> <source>Hide app screen in the recent apps.</source> - <target state="translated">ซ่อนหน้าจอแอพในแอพล่าสุด</target> + <target>ซ่อนหน้าจอแอพในแอพล่าสุด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hide profile" xml:space="preserve" approved="no"> + <trans-unit id="Hide profile" xml:space="preserve"> <source>Hide profile</source> - <target state="translated">ซ่อนโปรไฟล์</target> + <target>ซ่อนโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hide:" xml:space="preserve" approved="no"> + <trans-unit id="Hide:" xml:space="preserve"> <source>Hide:</source> - <target state="translated">ซ่อน:</target> + <target>ซ่อน:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="History" xml:space="preserve" approved="no"> + <trans-unit id="History" xml:space="preserve"> <source>History</source> - <target state="translated">ประวัติ</target> - <note>copied message info</note> + <target>ประวัติ</target> + <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="How SimpleX works" xml:space="preserve" approved="no"> + <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> - <target state="translated">วิธีการ SimpleX ทํางานอย่างไร</target> + <target>วิธีการ SimpleX ทํางานอย่างไร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="How it works" xml:space="preserve" approved="no"> + <trans-unit id="How it works" xml:space="preserve"> <source>How it works</source> - <target state="translated">มันทำงานอย่างไร</target> + <target>มันทำงานอย่างไร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="How to" xml:space="preserve" approved="no"> + <trans-unit id="How to" xml:space="preserve"> <source>How to</source> - <target state="translated">วิธี</target> + <target>วิธี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="How to use it" xml:space="preserve" approved="no"> + <trans-unit id="How to use it" xml:space="preserve"> <source>How to use it</source> - <target state="translated">วิธีการใช้งาน</target> + <target>วิธีการใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="How to use your servers" xml:space="preserve" approved="no"> + <trans-unit id="How to use your servers" xml:space="preserve"> <source>How to use your servers</source> - <target state="translated">วิธีใช้เซิร์ฟเวอร์ของคุณ</target> + <target>วิธีใช้เซิร์ฟเวอร์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="ICE servers (one per line)" xml:space="preserve" approved="no"> + <trans-unit id="ICE servers (one per line)" xml:space="preserve"> <source>ICE servers (one per line)</source> - <target state="translated">เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย)</target> + <target>เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve" approved="no"> + <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> <source>If you can't meet in person, show QR code in a video call, or share the link.</source> - <target state="translated">หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</target> + <target>หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve" approved="no"> + <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target state="translated">หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</target> + <target>หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve" approved="no"> + <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> - <target state="translated">หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</target> + <target>หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve" approved="no"> + <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> <source>If you enter your self-destruct passcode while opening the app:</source> - <target state="translated">หากคุณใส่รหัสทำลายตัวเองขณะเปิดแอป:</target> + <target>หากคุณใส่รหัสผ่านทำลายตัวเองขณะเปิดแอป:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve" approved="no"> + <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> - <target state="translated">หากคุณจำเป็นต้องใช้แชทตอนนี้ ให้แตะ **ทำในภายหลัง** ด้านล่าง (ระบบจะเสนอให้คุณย้ายฐานข้อมูลเมื่อคุณรีสตาร์ทแอป)</target> + <target>หากคุณจำเป็นต้องใช้แชทตอนนี้ ให้แตะ **ทำในภายหลัง** ด้านล่าง (ระบบจะเสนอให้คุณย้ายฐานข้อมูลเมื่อคุณรีสตาร์ทแอป)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Ignore" xml:space="preserve" approved="no"> + <trans-unit id="Ignore" xml:space="preserve"> <source>Ignore</source> - <target state="translated">ไม่สนใจ</target> + <target>ไม่สนใจ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve" approved="no"> + <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> <source>Image will be received when your contact completes uploading it.</source> - <target state="translated">จะได้รับภาพเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จสิ้น</target> + <target>จะได้รับภาพเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จสิ้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve" approved="no"> + <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Image will be received when your contact is online, please wait or check later!</source> - <target state="translated">จะได้รับรูปภาพเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</target> + <target>จะได้รับรูปภาพเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immediately" xml:space="preserve" approved="no"> + <trans-unit id="Immediately" xml:space="preserve"> <source>Immediately</source> - <target state="translated">โดยทันที</target> + <target>โดยทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no"> + <trans-unit id="Immune to spam and abuse" xml:space="preserve"> <source>Immune to spam and abuse</source> - <target state="translated">ป้องกันจากสแปมและการละเมิด</target> + <target>มีภูมิคุ้มกันต่อสแปมและการละเมิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Import" xml:space="preserve" approved="no"> + <trans-unit id="Import" xml:space="preserve"> <source>Import</source> - <target state="translated">นำเข้า</target> + <target>นำเข้า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Import chat database?" xml:space="preserve" approved="no"> + <trans-unit id="Import chat database?" xml:space="preserve"> <source>Import chat database?</source> - <target state="translated">นำเข้าฐานข้อมูลแชท?</target> + <target>นำเข้าฐานข้อมูลแชท?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Import database" xml:space="preserve" approved="no"> + <trans-unit id="Import database" xml:space="preserve"> <source>Import database</source> - <target state="translated">นำเข้าฐานข้อมูล</target> + <target>นำเข้าฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Improved privacy and security" xml:space="preserve" approved="no"> + <trans-unit id="Improved privacy and security" xml:space="preserve"> <source>Improved privacy and security</source> - <target state="translated">ปรับปรุงความเป็นส่วนตัวและความปลอดภัยแล้ว</target> + <target>ปรับปรุงความเป็นส่วนตัวและความปลอดภัยแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Improved server configuration" xml:space="preserve" approved="no"> + <trans-unit id="Improved server configuration" xml:space="preserve"> <source>Improved server configuration</source> - <target state="translated">ปรับปรุงการตั้งค่าเซิร์ฟเวอร์แล้ว</target> + <target>ปรับปรุงการกําหนดค่าเซิร์ฟเวอร์แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito" xml:space="preserve" approved="no"> + <trans-unit id="In reply to" xml:space="preserve"> + <source>In reply to</source> + <target>ในการตอบกลับถึง</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> - <target state="translated">ไม่ระบุตัวตน</target> + <target>ไม่ระบุตัวตน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode" xml:space="preserve" approved="no"> + <trans-unit id="Incognito mode" xml:space="preserve"> <source>Incognito mode</source> - <target state="translated">โหมดไม่ระบุตัวตน</target> + <target>โหมดไม่ระบุตัวตน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve" approved="no"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target state="translated">ไม่รองรับโหมดไม่ระบุตัวตนที่นี่ - โปรไฟล์หลักของคุณจะถูกส่งไปยังสมาชิกกลุ่ม</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve" approved="no"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target state="translated">โหมดไม่ระบุตัวตนปกป้องความเป็นส่วนตัวของชื่อและรูปภาพโปรไฟล์หลักของคุณ — สำหรับผู้ติดต่อใหม่แต่ละราย จะมีการสร้างโปรไฟล์ใหม่แบบสุ่ม</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incoming audio call" xml:space="preserve" approved="no"> + <trans-unit id="Incoming audio call" xml:space="preserve"> <source>Incoming audio call</source> - <target state="translated">สายเรียกเข้า</target> + <target>สายเรียกเข้า</target> <note>notification</note> </trans-unit> - <trans-unit id="Incoming call" xml:space="preserve" approved="no"> + <trans-unit id="Incoming call" xml:space="preserve"> <source>Incoming call</source> - <target state="translated">สายเรียกเข้า</target> + <target>สายเรียกเข้า</target> <note>notification</note> </trans-unit> - <trans-unit id="Incoming video call" xml:space="preserve" approved="no"> + <trans-unit id="Incoming video call" xml:space="preserve"> <source>Incoming video call</source> - <target state="translated">สายวิดีโอเข้ามา</target> + <target>สายวิดีโอเข้ามา</target> <note>notification</note> </trans-unit> - <trans-unit id="Incompatible database version" xml:space="preserve" approved="no"> + <trans-unit id="Incompatible database version" xml:space="preserve"> <source>Incompatible database version</source> - <target state="translated">เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้</target> + <target>เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incorrect passcode" xml:space="preserve" approved="no"> + <trans-unit id="Incorrect passcode" xml:space="preserve"> <source>Incorrect passcode</source> - <target state="translated">รหัสผ่านไม่ถูกต้อง</target> + <target>รหัสผ่านไม่ถูกต้อง</target> <note>PIN entry</note> </trans-unit> - <trans-unit id="Incorrect security code!" xml:space="preserve" approved="no"> + <trans-unit id="Incorrect security code!" xml:space="preserve"> <source>Incorrect security code!</source> - <target state="translated">รหัสความปลอดภัยไม่ถูกต้อง!</target> + <target>รหัสความปลอดภัยไม่ถูกต้อง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Info" xml:space="preserve" approved="no"> + <trans-unit id="Info" xml:space="preserve"> <source>Info</source> - <target state="translated">ข้อมูล</target> + <target>ข้อมูล</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Initial role" xml:space="preserve" approved="no"> + <trans-unit id="Initial role" xml:space="preserve"> <source>Initial role</source> - <target state="translated">บทบาทเริ่มต้น</target> + <target>บทบาทเริ่มต้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve" approved="no"> + <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> - <target state="translated">ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat)</target> + <target>ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve" approved="no"> + <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> <source>Instant push notifications will be hidden! </source> - <target state="translated">การแจ้งเตือนโดยทันทีจะถูกซ่อน! + <target>การแจ้งเตือนโดยทันทีจะถูกซ่อน! </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Instantly" xml:space="preserve" approved="no"> + <trans-unit id="Instantly" xml:space="preserve"> <source>Instantly</source> - <target state="translated">ทันที</target> + <target>ทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Interface" xml:space="preserve" approved="no"> + <trans-unit id="Interface" xml:space="preserve"> <source>Interface</source> - <target state="translated">อินเตอร์เฟซ</target> + <target>อินเตอร์เฟซ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Invalid connection link" xml:space="preserve" approved="no"> + <trans-unit id="Invalid connection link" xml:space="preserve"> <source>Invalid connection link</source> - <target state="translated">ลิงค์เชื่อมต่อไม่ถูกต้อง</target> + <target>ลิงค์เชื่อมต่อไม่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Invalid server address!" xml:space="preserve" approved="no"> + <trans-unit id="Invalid server address!" xml:space="preserve"> <source>Invalid server address!</source> - <target state="translated">ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target> + <target>ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Invitation expired!" xml:space="preserve" approved="no"> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <note>item status text</note> + </trans-unit> + <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> - <target state="translated">คำเชิญหมดอายุแล้ว!</target> + <target>คำเชิญหมดอายุแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Invite friends" xml:space="preserve" approved="no"> + <trans-unit id="Invite friends" xml:space="preserve"> <source>Invite friends</source> - <target state="translated">เชิญเพื่อนๆ</target> + <target>เชิญเพื่อนๆ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Invite members" xml:space="preserve" approved="no"> + <trans-unit id="Invite members" xml:space="preserve"> <source>Invite members</source> - <target state="translated">เชิญสมาชิก</target> + <target>เชิญสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Invite to group" xml:space="preserve" approved="no"> + <trans-unit id="Invite to group" xml:space="preserve"> <source>Invite to group</source> - <target state="translated">เชิญเข้าร่วมกลุ่ม</target> + <target>เชิญเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion" xml:space="preserve" approved="no"> + <trans-unit id="Irreversible message deletion" xml:space="preserve"> <source>Irreversible message deletion</source> - <target state="translated">การลบข้อความแบบแก้ไขไม่ได้</target> + <target>การลบข้อความแบบแก้ไขไม่ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve" approved="no"> + <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this chat.</source> - <target state="translated">ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้</target> + <target>ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no"> + <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> <source>Irreversible message deletion is prohibited in this group.</source> - <target state="translated">การลบข้อความแบบกลับไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้</target> + <target>การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve" approved="no"> + <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> - <target state="translated">อนุญาตให้มีการเชื่อมต่อที่ไม่ระบุตัวตนจำนวนมากโดยไม่มีข้อมูลที่ใช้ร่วมกันระหว่างกันในโปรไฟล์การแชทเดียว</target> + <target>อนุญาตให้มีการเชื่อมต่อที่ไม่ระบุตัวตนจำนวนมากโดยไม่มีข้อมูลที่ใช้ร่วมกันระหว่างกันในโปรไฟล์การแชทเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve" approved="no"> + <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> <source>It can happen when you or your connection used the old database backup.</source> - <target state="translated">สามารถเกิดขึ้นได้เมื่อคุณหรือการเชื่อมต่อของคุณใช้การสํารองข้อมูลฐานข้อมูลเก่า</target> + <target>สามารถเกิดขึ้นได้เมื่อคุณหรือการเชื่อมต่อของคุณใช้การสํารองข้อมูลฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve" approved="no"> + <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> <source>It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised.</source> - <target state="translated">มันสามารถเกิดขึ้นได้เมื่อ: + <target>มันสามารถเกิดขึ้นได้เมื่อ: 1. ข้อความหมดอายุในไคลเอนต์ที่ส่งหลังจาก 2 วันหรือบนเซิร์ฟเวอร์หลังจาก 30 วัน 2. การถอดรหัสข้อความล้มเหลว เนื่องจากคุณหรือผู้ติดต่อของคุณใช้การสำรองฐานข้อมูลเก่า 3. การเชื่อมต่อถูกบุกรุก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve" approved="no"> + <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> - <target state="translated">ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@).</target> + <target>ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Italian interface" xml:space="preserve" approved="no"> + <trans-unit id="Italian interface" xml:space="preserve"> <source>Italian interface</source> - <target state="translated">อินเทอร์เฟซภาษาอิตาลี</target> + <target>อินเทอร์เฟซภาษาอิตาลี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Japanese interface" xml:space="preserve" approved="no"> + <trans-unit id="Japanese interface" xml:space="preserve"> <source>Japanese interface</source> - <target state="translated">อินเทอร์เฟซภาษาญี่ปุ่น</target> + <target>อินเทอร์เฟซภาษาญี่ปุ่น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Join" xml:space="preserve" approved="no"> + <trans-unit id="Join" xml:space="preserve"> <source>Join</source> - <target state="translated">เข้าร่วม</target> + <target>เข้าร่วม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Join group" xml:space="preserve" approved="no"> + <trans-unit id="Join group" xml:space="preserve"> <source>Join group</source> - <target state="translated">เข้าร่วมกลุ่ม</target> + <target>เข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Join incognito" xml:space="preserve" approved="no"> + <trans-unit id="Join incognito" xml:space="preserve"> <source>Join incognito</source> - <target state="translated">เข้าร่วมแบบไม่ระบุตัวตน</target> + <target>เข้าร่วมแบบไม่ระบุตัวตน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Joining group" xml:space="preserve" approved="no"> + <trans-unit id="Joining group" xml:space="preserve"> <source>Joining group</source> - <target state="translated">กำลังจะเข้าร่วมกลุ่ม</target> + <target>กำลังจะเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="KeyChain error" xml:space="preserve" approved="no"> + <trans-unit id="Keep your connections" xml:space="preserve"> + <source>Keep your connections</source> + <target>รักษาการเชื่อมต่อของคุณ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="KeyChain error" xml:space="preserve"> <source>KeyChain error</source> - <target state="translated">ข้อผิดพลาดของ Keychain</target> + <target>ข้อผิดพลาดของ Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Keychain error" xml:space="preserve" approved="no"> + <trans-unit id="Keychain error" xml:space="preserve"> <source>Keychain error</source> - <target state="translated">ข้อผิดพลาดของ Keychain</target> + <target>ข้อผิดพลาดของ Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="LIVE" xml:space="preserve" approved="no"> + <trans-unit id="LIVE" xml:space="preserve"> <source>LIVE</source> - <target state="translated">สด</target> + <target>สด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Large file!" xml:space="preserve" approved="no"> + <trans-unit id="Large file!" xml:space="preserve"> <source>Large file!</source> - <target state="translated">ไฟล์ขนาดใหญ่!</target> + <target>ไฟล์ขนาดใหญ่!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Learn more" xml:space="preserve" approved="no"> + <trans-unit id="Learn more" xml:space="preserve"> <source>Learn more</source> - <target state="translated">ศึกษาเพิ่มเติม</target> + <target>ศึกษาเพิ่มเติม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave" xml:space="preserve" approved="no"> + <trans-unit id="Leave" xml:space="preserve"> <source>Leave</source> - <target state="translated">ออกจาก</target> + <target>ออกจาก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave group" xml:space="preserve" approved="no"> + <trans-unit id="Leave group" xml:space="preserve"> <source>Leave group</source> - <target state="translated">ออกจากกลุ่ม</target> + <target>ออกจากกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave group?" xml:space="preserve" approved="no"> + <trans-unit id="Leave group?" xml:space="preserve"> <source>Leave group?</source> - <target state="translated">ออกจากกลุ่ม?</target> + <target>ออกจากกลุ่ม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve" approved="no"> + <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> <source>Let's talk in SimpleX Chat</source> - <target state="translated">มาพูดคุยกันใน SimpleX Chat</target> + <target>มาคุยกันใน SimpleX Chat</target> <note>email subject</note> </trans-unit> - <trans-unit id="Light" xml:space="preserve" approved="no"> + <trans-unit id="Light" xml:space="preserve"> <source>Light</source> - <target state="translated">สว่าง</target> + <target>สว่าง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Limitations" xml:space="preserve" approved="no"> + <trans-unit id="Limitations" xml:space="preserve"> <source>Limitations</source> - <target state="translated">ข้อจำกัด</target> + <target>ข้อจำกัด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Live message!" xml:space="preserve" approved="no"> + <trans-unit id="Live message!" xml:space="preserve"> <source>Live message!</source> - <target state="translated">ข้อความสด!</target> + <target>ข้อความสด!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Live messages" xml:space="preserve" approved="no"> + <trans-unit id="Live messages" xml:space="preserve"> <source>Live messages</source> - <target state="translated">ข้อความสด</target> + <target>ข้อความสด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Local name" xml:space="preserve" approved="no"> + <trans-unit id="Local name" xml:space="preserve"> <source>Local name</source> - <target state="translated">ชื่อภายในเครื่องเท่านั้น</target> + <target>ชื่อภายในเครื่องเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Local profile data only" xml:space="preserve" approved="no"> + <trans-unit id="Local profile data only" xml:space="preserve"> <source>Local profile data only</source> - <target state="translated">ข้อมูลโปรไฟล์ภายในเครื่องเท่านั้น</target> + <target>ข้อมูลโปรไฟล์ภายในเครื่องเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Lock after" xml:space="preserve" approved="no"> + <trans-unit id="Lock after" xml:space="preserve"> <source>Lock after</source> - <target state="translated">ล็อคหลังจาก</target> + <target>ล็อคหลังจาก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Lock mode" xml:space="preserve" approved="no"> + <trans-unit id="Lock mode" xml:space="preserve"> <source>Lock mode</source> - <target state="translated">โหมดล็อค</target> + <target>โหมดล็อค</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve" approved="no"> + <trans-unit id="Make a private connection" xml:space="preserve"> <source>Make a private connection</source> - <target state="translated">สร้างการเชื่อมต่อส่วนตัว</target> + <target>สร้างการเชื่อมต่อแบบส่วนตัว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make profile private!" xml:space="preserve" approved="no"> + <trans-unit id="Make one message disappear" xml:space="preserve"> + <source>Make one message disappear</source> + <target>ทำให้ข้อความหายไปหนึ่งข้อความ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make profile private!" xml:space="preserve"> <source>Make profile private!</source> - <target state="translated">ทำให้โปรไฟล์เป็นส่วนตัว!</target> + <target>ทำให้โปรไฟล์เป็นส่วนตัว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve" approved="no"> + <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target state="translated">ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@)</target> + <target>ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve" approved="no"> + <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> - <target state="translated">ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน</target> + <target>ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve" approved="no"> + <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target state="translated">หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?*</target> + <target>หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mark deleted for everyone" xml:space="preserve" approved="no"> + <trans-unit id="Mark deleted for everyone" xml:space="preserve"> <source>Mark deleted for everyone</source> - <target state="translated">ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน</target> + <target>ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mark read" xml:space="preserve" approved="no"> + <trans-unit id="Mark read" xml:space="preserve"> <source>Mark read</source> - <target state="translated">ทำเครื่องหมายอ่านแล้ว</target> + <target>ทำเครื่องหมายอ่านแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mark verified" xml:space="preserve" approved="no"> + <trans-unit id="Mark verified" xml:space="preserve"> <source>Mark verified</source> - <target state="translated">ทำเครื่องหมายยืนยัน</target> + <target>ทําเครื่องหมายว่ายืนยันแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Markdown in messages" xml:space="preserve" approved="no"> + <trans-unit id="Markdown in messages" xml:space="preserve"> <source>Markdown in messages</source> - <target state="translated">Markdown ในข้อความ</target> + <target>Markdown ในข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve" approved="no"> + <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> <source>Max 30 seconds, received instantly.</source> - <target state="translated">สูงสุด 30 วินาที รับทันที</target> + <target>สูงสุด 30 วินาที รับทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member" xml:space="preserve" approved="no"> + <trans-unit id="Member" xml:space="preserve"> <source>Member</source> - <target state="translated">สมาชิก</target> + <target>สมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve" approved="no"> + <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> <source>Member role will be changed to "%@". All group members will be notified.</source> - <target state="translated">บทบาทของสมาชิกจะเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง</target> + <target>บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve" approved="no"> + <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> <source>Member role will be changed to "%@". The member will receive a new invitation.</source> - <target state="translated">บทบาทของสมาชิกจะเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่</target> + <target>บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกจะได้รับคำเชิญใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve" approved="no"> + <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> <source>Member will be removed from group - this cannot be undone!</source> - <target state="translated">สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!</target> + <target>สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message delivery error" xml:space="preserve" approved="no"> + <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> - <target state="translated">ข้อผิดพลาดในการส่งข้อความ</target> + <target>ข้อผิดพลาดในการส่งข้อความ</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve"> + <source>Message delivery receipts!</source> + <target>ใบเสร็จการส่งข้อความ!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message draft" xml:space="preserve" approved="no"> + <trans-unit id="Message draft" xml:space="preserve"> <source>Message draft</source> - <target state="translated">ร่างข้อความ</target> + <target>ร่างข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions" xml:space="preserve" approved="no"> + <trans-unit id="Message reactions" xml:space="preserve"> <source>Message reactions</source> - <target state="translated">ปฏิกิริยาของข้อความ</target> + <target>ปฏิกิริยาของข้อความ</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve" approved="no"> + <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> <source>Message reactions are prohibited in this chat.</source> - <target state="translated">ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้</target> + <target>ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no"> + <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> <source>Message reactions are prohibited in this group.</source> - <target state="translated">ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้</target> + <target>ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message text" xml:space="preserve" approved="no"> + <trans-unit id="Message text" xml:space="preserve"> <source>Message text</source> - <target state="translated">ข้อความ</target> + <target>ข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Messages" xml:space="preserve" approved="no"> + <trans-unit id="Messages" xml:space="preserve"> <source>Messages</source> - <target state="translated">ข้อความ</target> + <target>ข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Messages & files" xml:space="preserve" approved="no"> + <trans-unit id="Messages & files" xml:space="preserve"> <source>Messages & files</source> - <target state="translated">ข้อความและไฟล์</target> + <target>ข้อความและไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve" approved="no"> - <source>Migrating database archive...</source> - <target state="translated">กำลังย้ายข้อมูลที่เก็บของฐานข้อมูล...</target> + <trans-unit id="Migrating database archive…" xml:space="preserve"> + <source>Migrating database archive…</source> + <target>กำลังย้ายข้อมูลที่เก็บถาวรของฐานข้อมูล…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migration error:" xml:space="preserve" approved="no"> + <trans-unit id="Migration error:" xml:space="preserve"> <source>Migration error:</source> - <target state="translated">ข้อผิดพลาดในการย้ายข้อมูล:</target> + <target>ข้อผิดพลาดในการย้ายข้อมูล:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve" approved="no"> + <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> - <target state="translated">การย้ายข้อมูลล้มเหลว แตะ **ข้าม** ด้านล่างเพื่อใช้ฐานข้อมูลปัจจุบันต่อไป โปรดรายงานปัญหาให้นักพัฒนาแอปทราบผ่านทางแชทหรืออีเมล [chat@simplex.chat](mailto:chat@simplex.chat)</target> + <target>การย้ายข้อมูลล้มเหลว แตะ **ข้าม** ด้านล่างเพื่อใช้ฐานข้อมูลปัจจุบันต่อไป โปรดรายงานปัญหาให้นักพัฒนาแอปทราบผ่านทางแชทหรืออีเมล [chat@simplex.chat](mailto:chat@simplex.chat)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migration is completed" xml:space="preserve" approved="no"> + <trans-unit id="Migration is completed" xml:space="preserve"> <source>Migration is completed</source> - <target state="translated">การโยกย้ายเสร็จสมบูรณ์</target> + <target>การโยกย้ายเสร็จสมบูรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve" approved="no"> + <trans-unit id="Migrations: %@" xml:space="preserve"> <source>Migrations: %@</source> - <target state="translated">การย้ายข้อมูล: %@</target> + <target>การย้ายข้อมูล: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Moderate" xml:space="preserve" approved="no"> + <trans-unit id="Moderate" xml:space="preserve"> <source>Moderate</source> - <target state="translated">กลั่นกรอง</target> + <target>กลั่นกรอง</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Moderated at" xml:space="preserve" approved="no"> + <trans-unit id="Moderated at" xml:space="preserve"> <source>Moderated at</source> - <target state="translated">กลั่นกรองที่</target> + <target>กลั่นกรองที่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Moderated at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Moderated at: %@" xml:space="preserve"> <source>Moderated at: %@</source> - <target state="translated">กลั่นกรองที่: %@</target> + <target>กลั่นกรองที่: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="More improvements are coming soon!" xml:space="preserve" approved="no"> + <trans-unit id="More improvements are coming soon!" xml:space="preserve"> <source>More improvements are coming soon!</source> - <target state="translated">การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</target> + <target>การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve" approved="no"> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <note>item status description</note> + </trans-unit> + <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> - <target state="translated">เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ</target> + <target>เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Multiple chat profiles" xml:space="preserve" approved="no"> + <trans-unit id="Multiple chat profiles" xml:space="preserve"> <source>Multiple chat profiles</source> - <target state="translated">โปรไฟล์การแชทหลายรายการ</target> + <target>โปรไฟล์การแชทหลายรายการ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Mute" xml:space="preserve" approved="no"> + <trans-unit id="Mute" xml:space="preserve"> <source>Mute</source> - <target state="translated">ปิดเสียง</target> + <target>ปิดเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Muted when inactive!" xml:space="preserve" approved="no"> + <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> - <target state="translated">ปิดเสียงเมื่อไม่ได้ใช้งาน!</target> + <target>ปิดเสียงเมื่อไม่ได้ใช้งาน!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Name" xml:space="preserve" approved="no"> + <trans-unit id="Name" xml:space="preserve"> <source>Name</source> - <target state="translated">ชื่อ</target> + <target>ชื่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Network & servers" xml:space="preserve" approved="no"> + <trans-unit id="Network & servers" xml:space="preserve"> <source>Network & servers</source> - <target state="translated">เครือข่ายและเซิร์ฟเวอร์</target> + <target>เครือข่ายและเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Network settings" xml:space="preserve" approved="no"> + <trans-unit id="Network settings" xml:space="preserve"> <source>Network settings</source> - <target state="translated">การตั้งค่าเครือข่าย</target> + <target>การตั้งค่าเครือข่าย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Network status" xml:space="preserve" approved="no"> + <trans-unit id="Network status" xml:space="preserve"> <source>Network status</source> - <target state="translated">สถานะเครือข่าย</target> + <target>สถานะเครือข่าย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New Passcode" xml:space="preserve" approved="no"> + <trans-unit id="New Passcode" xml:space="preserve"> <source>New Passcode</source> - <target state="translated">รหัสผ่านใหม่</target> + <target>รหัสผ่านใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New contact request" xml:space="preserve" approved="no"> + <trans-unit id="New contact request" xml:space="preserve"> <source>New contact request</source> - <target state="translated">คำขอติดต่อใหม่</target> + <target>คำขอติดต่อใหม่</target> <note>notification</note> </trans-unit> - <trans-unit id="New contact:" xml:space="preserve" approved="no"> + <trans-unit id="New contact:" xml:space="preserve"> <source>New contact:</source> - <target state="translated">คำขอติดต่อใหม่:</target> + <target>คำขอติดต่อใหม่:</target> <note>notification</note> </trans-unit> - <trans-unit id="New database archive" xml:space="preserve" approved="no"> + <trans-unit id="New database archive" xml:space="preserve"> <source>New database archive</source> - <target state="translated">ฐานข้อมูลใหม่สำหรับการเก็บถาวร</target> + <target>ฐานข้อมูลใหม่สำหรับการเก็บถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New display name" xml:space="preserve" approved="no"> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> - <target state="translated">ชื่อที่แสดงใหม่</target> + <target>ชื่อที่แสดงใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New in %@" xml:space="preserve" approved="no"> + <trans-unit id="New in %@" xml:space="preserve"> <source>New in %@</source> - <target state="translated">ใหม่ใน %@</target> + <target>ใหม่ใน %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New member role" xml:space="preserve" approved="no"> + <trans-unit id="New member role" xml:space="preserve"> <source>New member role</source> - <target state="translated">บทบาทของสมาชิกใหม่</target> + <target>บทบาทของสมาชิกใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New message" xml:space="preserve" approved="no"> + <trans-unit id="New message" xml:space="preserve"> <source>New message</source> - <target state="translated">ข้อความใหม่</target> + <target>ข้อความใหม่</target> <note>notification</note> </trans-unit> - <trans-unit id="New passphrase…" xml:space="preserve" approved="no"> + <trans-unit id="New passphrase…" xml:space="preserve"> <source>New passphrase…</source> - <target state="translated">รหัสผ่านใหม่…</target> + <target>รหัสผ่านใหม่…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No" xml:space="preserve" approved="no"> + <trans-unit id="No" xml:space="preserve"> <source>No</source> - <target state="translated">เลขที่</target> + <target>เลขที่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No app password" xml:space="preserve" approved="no"> + <trans-unit id="No app password" xml:space="preserve"> <source>No app password</source> - <target state="translated">ไม่มีรหัสผ่านสำหรับแอป</target> + <target>ไม่มีรหัสผ่านสำหรับแอป</target> <note>Authentication unavailable</note> </trans-unit> - <trans-unit id="No contacts selected" xml:space="preserve" approved="no"> + <trans-unit id="No contacts selected" xml:space="preserve"> <source>No contacts selected</source> - <target state="translated">ไม่ได้เลือกผู้ติดต่อ</target> + <target>ไม่ได้เลือกผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No contacts to add" xml:space="preserve" approved="no"> + <trans-unit id="No contacts to add" xml:space="preserve"> <source>No contacts to add</source> - <target state="translated">ไม่มีรายชื่อที่จะเพิ่ม</target> + <target>ไม่มีรายชื่อที่จะเพิ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No device token!" xml:space="preserve" approved="no"> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> - <target state="translated">ไม่มีโทเค็นอุปกรณ์!</target> + <target>ไม่มีโทเค็นอุปกรณ์!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No group!" xml:space="preserve" approved="no"> + <trans-unit id="No filtered chats" xml:space="preserve"> + <source>No filtered chats</source> + <target>ไม่มีการกรองการแชท</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No group!" xml:space="preserve"> <source>Group not found!</source> - <target state="translated">ไม่พบกลุ่ม!</target> + <target>ไม่พบกลุ่ม!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No permission to record voice message" xml:space="preserve" approved="no"> + <trans-unit id="No history" xml:space="preserve"> + <source>No history</source> + <target>ไม่มีประวัติ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No permission to record voice message" xml:space="preserve"> <source>No permission to record voice message</source> - <target state="translated">ไม่อนุญาตให้บันทึกข้อความเสียง</target> + <target>ไม่อนุญาตให้บันทึกข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No received or sent files" xml:space="preserve" approved="no"> + <trans-unit id="No received or sent files" xml:space="preserve"> <source>No received or sent files</source> - <target state="translated">ไม่มีไฟล์ที่ได้รับหรือส่ง</target> + <target>ไม่มีไฟล์ที่ได้รับหรือส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Notifications" xml:space="preserve" approved="no"> + <trans-unit id="Notifications" xml:space="preserve"> <source>Notifications</source> - <target state="translated">การแจ้งเตือน</target> + <target>การแจ้งเตือน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Notifications are disabled!" xml:space="preserve" approved="no"> + <trans-unit id="Notifications are disabled!" xml:space="preserve"> <source>Notifications are disabled!</source> - <target state="translated">ปิดการแจ้งเตือน!</target> + <target>ปิดการแจ้งเตือน!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve" approved="no"> + <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> - <target state="translated">ขณะนี้ผู้ดูแลระบบสามารถ: + <target>ขณะนี้ผู้ดูแลระบบสามารถ: - ลบข้อความของสมาชิก - ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Off" xml:space="preserve" approved="no"> + <trans-unit id="Off" xml:space="preserve"> <source>Off</source> - <target state="translated">ปิด</target> + <target>ปิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve" approved="no"> + <trans-unit id="Off (Local)" xml:space="preserve"> <source>Off (Local)</source> - <target state="translated">ปิด (ในเครื่อง)</target> + <target>ปิด (ในเครื่อง)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Ok" xml:space="preserve" approved="no"> + <trans-unit id="Ok" xml:space="preserve"> <source>Ok</source> - <target state="translated">ตกลง</target> + <target>ตกลง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database" xml:space="preserve" approved="no"> + <trans-unit id="Old database" xml:space="preserve"> <source>Old database</source> - <target state="translated">ฐานข้อมูลเก่า</target> + <target>ฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve" approved="no"> + <trans-unit id="Old database archive" xml:space="preserve"> <source>Old database archive</source> - <target state="translated">คลังฐานข้อมูลเก่า</target> + <target>คลังฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="One-time invitation link" xml:space="preserve" approved="no"> + <trans-unit id="One-time invitation link" xml:space="preserve"> <source>One-time invitation link</source> - <target state="translated">ลิงก์คำเชิญแบบครั้งเดียว</target> + <target>ลิงก์คำเชิญแบบใช้ครั้งเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve" approved="no"> + <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target state="translated">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ</target> + <target>จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve" approved="no"> + <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target state="translated">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ</target> + <target>จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Onion hosts will not be used." xml:space="preserve" approved="no"> + <trans-unit id="Onion hosts will not be used." xml:space="preserve"> <source>Onion hosts will not be used.</source> - <target state="translated">โฮสต์หัวหอมจะไม่ถูกใช้</target> + <target>โฮสต์หัวหอมจะไม่ถูกใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve" approved="no"> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> - <target state="translated">เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**</target> + <target>เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only group owners can change group preferences." xml:space="preserve" approved="no"> + <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> <source>Only group owners can change group preferences.</source> - <target state="translated">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนการตั้งค่ากลุ่มได้</target> + <target>เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only group owners can enable voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> + <source>Only group owners can enable files and media.</source> + <target>เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานไฟล์และสื่อได้</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> <source>Only group owners can enable voice messages.</source> - <target state="translated">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้</target> + <target>เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can add message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Only you can add message reactions." xml:space="preserve"> <source>Only you can add message reactions.</source> - <target state="translated">มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้</target> + <target>มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve" approved="no"> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target state="translated">มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)</target> + <target>มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can make calls." xml:space="preserve" approved="no"> + <trans-unit id="Only you can make calls." xml:space="preserve"> <source>Only you can make calls.</source> - <target state="translated">มีเพียงคุณเท่านั้นที่โทรออกได้</target> + <target>มีเพียงคุณเท่านั้นที่โทรออกได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can send disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> <source>Only you can send disappearing messages.</source> - <target state="translated">มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> + <target>มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can send voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Only you can send voice messages." xml:space="preserve"> <source>Only you can send voice messages.</source> - <target state="translated">มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</target> + <target>มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can add message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> <source>Only your contact can add message reactions.</source> - <target state="translated">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้</target> + <target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve" approved="no"> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target state="translated">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)</target> + <target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can make calls." xml:space="preserve" approved="no"> + <trans-unit id="Only your contact can make calls." xml:space="preserve"> <source>Only your contact can make calls.</source> - <target state="translated">ผู้ติดต่อของคุณเท่านั้นที่สามารถโทรออกได้</target> + <target>ผู้ติดต่อของคุณเท่านั้นที่สามารถโทรออกได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> <source>Only your contact can send disappearing messages.</source> - <target state="translated">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> + <target>เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can send voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> <source>Only your contact can send voice messages.</source> - <target state="translated">ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</target> + <target>ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open Settings" xml:space="preserve" approved="no"> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> - <target state="translated">เปิดการตั้งค่า</target> + <target>เปิดการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open chat" xml:space="preserve" approved="no"> + <trans-unit id="Open chat" xml:space="preserve"> <source>Open chat</source> - <target state="translated">เปิดแชท</target> + <target>เปิดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Open chat console" xml:space="preserve" approved="no"> + <trans-unit id="Open chat console" xml:space="preserve"> <source>Open chat console</source> - <target state="translated">เปิดคอนโซลการแชท</target> + <target>เปิดคอนโซลการแชท</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve" approved="no"> + <trans-unit id="Open user profiles" xml:space="preserve"> <source>Open user profiles</source> - <target state="translated">เปิดโปรไฟล์ผู้ใช้</target> + <target>เปิดโปรไฟล์ผู้ใช้</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve" approved="no"> + <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> <source>Open-source protocol and code – anybody can run the servers.</source> - <target state="translated">โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้</target> + <target>โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve" approved="no"> + <trans-unit id="Opening database…" xml:space="preserve"> <source>Opening database…</source> - <target state="translated">กำลังเปิดฐานข้อมูล…</target> + <target>กำลังเปิดฐานข้อมูล…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve" approved="no"> + <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> - <target state="translated">การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง</target> + <target>การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="PING count" xml:space="preserve" approved="no"> + <trans-unit id="PING count" xml:space="preserve"> <source>PING count</source> - <target state="translated">จํานวน PING</target> + <target>จํานวน PING</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="PING interval" xml:space="preserve" approved="no"> + <trans-unit id="PING interval" xml:space="preserve"> <source>PING interval</source> - <target state="translated">ช่วงเวลา PING</target> + <target>ช่วงเวลา PING</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode" xml:space="preserve" approved="no"> + <trans-unit id="Passcode" xml:space="preserve"> <source>Passcode</source> - <target state="translated">รหัสผ่าน</target> + <target>รหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode changed!" xml:space="preserve" approved="no"> + <trans-unit id="Passcode changed!" xml:space="preserve"> <source>Passcode changed!</source> - <target state="translated">เปลี่ยนรหัสผ่านแล้ว!</target> + <target>เปลี่ยนรหัสผ่านแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode entry" xml:space="preserve" approved="no"> + <trans-unit id="Passcode entry" xml:space="preserve"> <source>Passcode entry</source> - <target state="translated">การใส่รหัสผ่าน</target> + <target>การใส่รหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode not changed!" xml:space="preserve" approved="no"> + <trans-unit id="Passcode not changed!" xml:space="preserve"> <source>Passcode not changed!</source> - <target state="translated">รหัสผ่านไม่เปลี่ยน!</target> + <target>รหัสผ่านไม่เปลี่ยน!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode set!" xml:space="preserve" approved="no"> + <trans-unit id="Passcode set!" xml:space="preserve"> <source>Passcode set!</source> - <target state="translated">ตั้งรหัสผ่านเรียบร้อยแล้ว!</target> + <target>ตั้งรหัสผ่านเรียบร้อยแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Password to show" xml:space="preserve" approved="no"> + <trans-unit id="Password to show" xml:space="preserve"> <source>Password to show</source> - <target state="translated">รหัสผ่านที่จะแสดง</target> + <target>รหัสผ่านที่จะแสดง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste" xml:space="preserve" approved="no"> + <trans-unit id="Paste" xml:space="preserve"> <source>Paste</source> - <target state="translated">แปะ</target> + <target>แปะ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste image" xml:space="preserve" approved="no"> + <trans-unit id="Paste image" xml:space="preserve"> <source>Paste image</source> - <target state="translated">แปะภาพ</target> + <target>แปะภาพ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve" approved="no"> + <trans-unit id="Paste received link" xml:space="preserve"> <source>Paste received link</source> - <target state="translated">แปะลิงก์ที่ได้รับ</target> + <target>แปะลิงก์ที่ได้รับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve" approved="no"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <target state="translated">แปะลิงก์ที่คุณได้รับลงในช่องด้านล่างเพื่อเชื่อมต่อกับผู้ติดต่อของคุณ</target> - <note>No comment provided by engineer.</note> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> + <note>placeholder</note> </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve" approved="no"> + <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> - <target state="translated">ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</target> + <target>ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Periodically" xml:space="preserve" approved="no"> + <trans-unit id="Periodically" xml:space="preserve"> <source>Periodically</source> - <target state="translated">เป็นระยะๆ</target> + <target>เป็นระยะๆ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Permanent decryption error" xml:space="preserve" approved="no"> + <trans-unit id="Permanent decryption error" xml:space="preserve"> <source>Permanent decryption error</source> - <target state="translated">ข้อผิดพลาดในการถอดรหัสอย่างถาวร</target> + <target>ข้อผิดพลาดในการถอดรหัสอย่างถาวร</target> <note>message decrypt error item</note> </trans-unit> - <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> <source>Please ask your contact to enable sending voice messages.</source> - <target state="translated">โปรดขอให้ผู้ติดต่อของคุณเปิดใช้งานการส่งข้อความเสียง</target> + <target>โปรดขอให้ผู้ติดต่อของคุณเปิดใช้งานการส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve" approved="no"> + <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> <source>Please check that you used the correct link or ask your contact to send you another one.</source> - <target state="translated">โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ</target> + <target>โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve" approved="no"> + <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> <source>Please check your network connection with %@ and try again.</source> - <target state="translated">โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณกับ %@ แล้วลองอีกครั้ง</target> + <target>โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณกับ %@ แล้วลองอีกครั้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please check yours and your contact preferences." xml:space="preserve" approved="no"> + <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> <source>Please check yours and your contact preferences.</source> - <target state="translated">โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ</target> + <target>โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please contact group admin." xml:space="preserve" approved="no"> + <trans-unit id="Please contact group admin." xml:space="preserve"> <source>Please contact group admin.</source> - <target state="translated">โปรดติดต่อผู้ดูแลกลุ่ม</target> + <target>โปรดติดต่อผู้ดูแลกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please enter correct current passphrase." xml:space="preserve" approved="no"> + <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> <source>Please enter correct current passphrase.</source> - <target state="translated">โปรดใส่รหัสผ่านปัจจุบันที่ถูกต้อง</target> + <target>โปรดใส่รหัสผ่านปัจจุบันที่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve" approved="no"> + <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> - <target state="translated">โปรดใส่รหัสผ่านก่อนหน้านี้หลังจากกู้คืนข้อมูลสํารองฐานข้อมูล การกระทํานี้ไม่สามารถยกเลิกได้</target> + <target>โปรดใส่รหัสผ่านก่อนหน้านี้หลังจากกู้คืนข้อมูลสํารองฐานข้อมูล การกระทํานี้ไม่สามารถยกเลิกได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve" approved="no"> + <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> - <target state="translated">โปรดจำหรือเก็บไว้อย่างปลอดภัย - ไม่มีทางกู้คืนรหัสผ่านที่หายไปได้!</target> + <target>โปรดจำหรือเก็บไว้อย่างปลอดภัย - ไม่มีทางกู้คืนรหัสผ่านที่หายไปได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please report it to the developers." xml:space="preserve" approved="no"> + <trans-unit id="Please report it to the developers." xml:space="preserve"> <source>Please report it to the developers.</source> - <target state="translated">โปรดรายงานไปยังผู้พัฒนาแอป</target> + <target>โปรดรายงานไปยังผู้พัฒนาแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve" approved="no"> + <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> <source>Please restart the app and migrate the database to enable push notifications.</source> - <target state="translated">โปรดรีสตาร์ทแอปและย้ายฐานข้อมูลเพื่อเปิดใช้งานการแจ้งเตือนแบบทันที</target> + <target>โปรดรีสตาร์ทแอปและย้ายฐานข้อมูลเพื่อเปิดใช้งานการแจ้งเตือนแบบทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve" approved="no"> + <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> - <target state="translated">โปรดเก็บรหัสผ่านไว้อย่างปลอดภัย คุณจะไม่สามารถเข้าถึงแชทได้หากทำหาย</target> + <target>โปรดเก็บรหัสผ่านไว้อย่างปลอดภัย คุณจะไม่สามารถเข้าถึงแชทได้หากทำหาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve" approved="no"> + <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> - <target state="translated">โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target> + <target>โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Polish interface" xml:space="preserve" approved="no"> + <trans-unit id="Polish interface" xml:space="preserve"> <source>Polish interface</source> - <target state="translated">อินเตอร์เฟซภาษาโปแลนด์</target> + <target>อินเตอร์เฟซภาษาโปแลนด์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve" approved="no"> + <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> <source>Possibly, certificate fingerprint in server address is incorrect</source> - <target state="translated">อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง</target> + <target>อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง</target> <note>server test error</note> </trans-unit> - <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve" approved="no"> + <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> <source>Preserve the last message draft, with attachments.</source> - <target state="translated">เก็บร่างข้อความล่าสุดพร้อมไฟล์แนบ</target> + <target>เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server" xml:space="preserve" approved="no"> + <trans-unit id="Preset server" xml:space="preserve"> <source>Preset server</source> - <target state="translated">เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> + <target>เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preset server address" xml:space="preserve" approved="no"> + <trans-unit id="Preset server address" xml:space="preserve"> <source>Preset server address</source> - <target state="translated">ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> + <target>ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Preview" xml:space="preserve" approved="no"> + <trans-unit id="Preview" xml:space="preserve"> <source>Preview</source> - <target state="translated">ดูตัวอย่าง</target> + <target>ดูตัวอย่าง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Privacy & security" xml:space="preserve" approved="no"> + <trans-unit id="Privacy & security" xml:space="preserve"> <source>Privacy & security</source> - <target state="translated">ความเป็นส่วนตัวและความปลอดภัย</target> + <target>ความเป็นส่วนตัวและความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Privacy redefined" xml:space="preserve" approved="no"> + <trans-unit id="Privacy redefined" xml:space="preserve"> <source>Privacy redefined</source> - <target state="translated">นิยามความเป็นส่วนตัวใหม่</target> + <target>นิยามความเป็นส่วนตัวใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Private filenames" xml:space="preserve" approved="no"> + <trans-unit id="Private filenames" xml:space="preserve"> <source>Private filenames</source> - <target state="translated">ชื่อไฟล์ส่วนตัว</target> + <target>ชื่อไฟล์ส่วนตัว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile and server connections" xml:space="preserve" approved="no"> + <trans-unit id="Profile and server connections" xml:space="preserve"> <source>Profile and server connections</source> - <target state="translated">การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์</target> + <target>การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile image" xml:space="preserve" approved="no"> + <trans-unit id="Profile image" xml:space="preserve"> <source>Profile image</source> - <target state="translated">รูปโปรไฟล์</target> + <target>รูปโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile password" xml:space="preserve" approved="no"> + <trans-unit id="Profile password" xml:space="preserve"> <source>Profile password</source> - <target state="translated">รหัสผ่านโปรไฟล์</target> + <target>รหัสผ่านโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve" approved="no"> + <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> <source>Profile update will be sent to your contacts.</source> - <target state="translated">การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target> + <target>การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit audio/video calls." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> <source>Prohibit audio/video calls.</source> - <target state="translated">ห้ามการโทรด้วยเสียง/วิดีโอ</target> + <target>ห้ามการโทรด้วยเสียง/วิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> <source>Prohibit irreversible message deletion.</source> - <target state="translated">ห้ามการลบข้อความที่ย้อนกลับไม่ได้</target> + <target>ห้ามการลบข้อความที่ย้อนกลับไม่ได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit message reactions." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit message reactions." xml:space="preserve"> <source>Prohibit message reactions.</source> - <target state="translated">ห้ามแสดงปฏิกิริยาต่อข้อความ</target> + <target>ห้ามแสดงปฏิกิริยาต่อข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit messages reactions." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit messages reactions." xml:space="preserve"> <source>Prohibit messages reactions.</source> - <target state="translated">ห้ามแสดงปฏิกิริยาต่อข้อความ</target> + <target>ห้ามแสดงปฏิกิริยาต่อข้อความ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> <source>Prohibit sending direct messages to members.</source> - <target state="translated">ห้ามส่งข้อความส่วนตัวถึงสมาชิก</target> + <target>ห้ามส่งข้อความโดยตรงถึงสมาชิก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> <source>Prohibit sending disappearing messages.</source> - <target state="translated">ห้ามส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages)</target> + <target>ห้ามส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit sending voice messages." xml:space="preserve" approved="no"> + <trans-unit id="Prohibit sending files and media." xml:space="preserve"> + <source>Prohibit sending files and media.</source> + <target>ห้ามส่งไฟล์และสื่อ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> <source>Prohibit sending voice messages.</source> - <target state="translated">ห้ามส่งข้อความเสียง</target> + <target>ห้ามส่งข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Protect app screen" xml:space="preserve" approved="no"> + <trans-unit id="Protect app screen" xml:space="preserve"> <source>Protect app screen</source> - <target state="translated">ปกป้องหน้าจอแอป</target> + <target>ปกป้องหน้าจอแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve" approved="no"> + <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> <source>Protect your chat profiles with a password!</source> - <target state="translated">ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน!</target> + <target>ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Protocol timeout" xml:space="preserve" approved="no"> + <trans-unit id="Protocol timeout" xml:space="preserve"> <source>Protocol timeout</source> - <target state="translated">หมดเวลาโปรโตคอล</target> + <target>หมดเวลาโปรโตคอล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Push notifications" xml:space="preserve" approved="no"> + <trans-unit id="Protocol timeout per KB" xml:space="preserve"> + <source>Protocol timeout per KB</source> + <target>การหมดเวลาของโปรโตคอลต่อ KB</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push notifications" xml:space="preserve"> <source>Push notifications</source> - <target state="translated">การแจ้งเตือนแบบทันที</target> + <target>การแจ้งเตือนแบบทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Rate the app" xml:space="preserve" approved="no"> + <trans-unit id="Rate the app" xml:space="preserve"> <source>Rate the app</source> - <target state="translated">ให้คะแนนแอป</target> + <target>ให้คะแนนแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="React..." xml:space="preserve" approved="no"> - <source>React...</source> - <target state="translated">ปฏิกิริยา...</target> + <trans-unit id="React…" xml:space="preserve"> + <source>React…</source> + <target>ตอบสนอง…</target> <note>chat item menu</note> </trans-unit> - <trans-unit id="Read" xml:space="preserve" approved="no"> + <trans-unit id="Read" xml:space="preserve"> <source>Read</source> - <target state="translated">อ่าน</target> + <target>อ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more" xml:space="preserve" approved="no"> + <trans-unit id="Read more" xml:space="preserve"> <source>Read more</source> - <target state="translated">อ่านเพิ่มเติม</target> + <target>อ่านเพิ่มเติม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve" approved="no"> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target state="translated">อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)</target> + <target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve" approved="no"> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> - <target state="translated">อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)</target> + <target>อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve" approved="no"> + <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> <source>Read more in our GitHub repository.</source> - <target state="translated">อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา</target> + <target>อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve" approved="no"> + <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> - <target state="translated">อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme)</target> + <target>อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Received at" xml:space="preserve" approved="no"> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> - <target state="translated">ได้รับเมื่อ</target> + <target>ได้รับเมื่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Received at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Received at: %@" xml:space="preserve"> <source>Received at: %@</source> - <target state="translated">ได้รับเมื่อ: %@</target> + <target>ได้รับเมื่อ: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Received file event" xml:space="preserve" approved="no"> + <trans-unit id="Received file event" xml:space="preserve"> <source>Received file event</source> - <target state="translated">ได้รับไฟล์</target> + <target>ได้รับไฟล์</target> <note>notification</note> </trans-unit> - <trans-unit id="Received message" xml:space="preserve" approved="no"> + <trans-unit id="Received message" xml:space="preserve"> <source>Received message</source> - <target state="translated">ได้รับข้อความ</target> + <target>ได้รับข้อความ</target> <note>message info title</note> </trans-unit> - <trans-unit id="Receiving file will be stopped." xml:space="preserve" approved="no"> + <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> + <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving file will be stopped." xml:space="preserve"> <source>Receiving file will be stopped.</source> - <target state="translated">การรับไฟล์จะหยุดลง</target> + <target>การรับไฟล์จะหยุดลง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Receiving via" xml:space="preserve" approved="no"> + <trans-unit id="Receiving via" xml:space="preserve"> <source>Receiving via</source> - <target state="translated">กำลังจะรับทาง</target> + <target>กำลังรับผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Recipients see updates as you type them." xml:space="preserve" approved="no"> + <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> <source>Recipients see updates as you type them.</source> - <target state="translated">ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</target> + <target>ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Record updated at" xml:space="preserve" approved="no"> + <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>เชื่อมต่อเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดอีกครั้งเพื่อบังคับให้ส่งข้อความ มันใช้การจราจรเพิ่มเติม</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect servers?" xml:space="preserve"> + <source>Reconnect servers?</source> + <target>เชื่อมต่อเซิร์ฟเวอร์อีกครั้งหรือไม่?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at" xml:space="preserve"> <source>Record updated at</source> - <target state="translated">บันทึกถูกอัปเดตเมื่อ</target> + <target>บันทึกถูกอัปเดตเมื่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Record updated at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Record updated at: %@" xml:space="preserve"> <source>Record updated at: %@</source> - <target state="translated">บันทึกถูกอัปเดตเมื่อ: %@</target> + <target>บันทึกถูกอัปเดตเมื่อ: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Reduced battery usage" xml:space="preserve" approved="no"> + <trans-unit id="Reduced battery usage" xml:space="preserve"> <source>Reduced battery usage</source> - <target state="translated">ลดการใช้แบตเตอรี่</target> + <target>ลดการใช้แบตเตอรี่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reject" xml:space="preserve" approved="no"> + <trans-unit id="Reject" xml:space="preserve"> <source>Reject</source> - <target state="translated">ปฏิเสธ</target> + <target>ปฏิเสธ</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve" approved="no"> - <source>Reject contact (sender NOT notified)</source> - <target state="translated">ปฏิเสธการติดต่อ (ผู้ส่งไม่ได้รับแจ้ง)</target> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reject contact request" xml:space="preserve" approved="no"> + <trans-unit id="Reject contact request" xml:space="preserve"> <source>Reject contact request</source> - <target state="translated">ปฏิเสธคำขอติดต่อ</target> + <target>ปฏิเสธคำขอติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve" approved="no"> + <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> <source>Relay server is only used if necessary. Another party can observe your IP address.</source> - <target state="translated">ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้</target> + <target>ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve" approved="no"> + <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> <source>Relay server protects your IP address, but it can observe the duration of the call.</source> - <target state="translated">เซิร์ฟเวอร์รีเลย์ปกป้องที่อยู่ IP ของคุณ แต่สามารถสังเกตระยะเวลาของการโทรได้</target> + <target>เซิร์ฟเวอร์รีเลย์ปกป้องที่อยู่ IP ของคุณ แต่สามารถสังเกตระยะเวลาของการโทรได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Remove" xml:space="preserve" approved="no"> + <trans-unit id="Remove" xml:space="preserve"> <source>Remove</source> - <target state="translated">ลบ</target> + <target>ลบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Remove member" xml:space="preserve" approved="no"> + <trans-unit id="Remove member" xml:space="preserve"> <source>Remove member</source> - <target state="translated">ลบสมาชิกออก</target> + <target>ลบสมาชิกออก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Remove member?" xml:space="preserve" approved="no"> + <trans-unit id="Remove member?" xml:space="preserve"> <source>Remove member?</source> - <target state="translated">ลบสมาชิกออก?</target> + <target>ลบสมาชิกออก?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Remove passphrase from keychain?" xml:space="preserve" approved="no"> + <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> <source>Remove passphrase from keychain?</source> - <target state="translated">ลบรหัสผ่านออกจาก keychain หรือไม่?</target> + <target>ลบรหัสผ่านออกจาก keychain หรือไม่?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reply" xml:space="preserve" approved="no"> + <trans-unit id="Renegotiate" xml:space="preserve"> + <source>Renegotiate</source> + <target>เจรจาใหม่</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption" xml:space="preserve"> + <source>Renegotiate encryption</source> + <target>เจรจา encryption อีกครั้ง</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption?" xml:space="preserve"> + <source>Renegotiate encryption?</source> + <target>เจรจา enryption ใหม่หรือไม่?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reply" xml:space="preserve"> <source>Reply</source> - <target state="translated">ตอบ</target> + <target>ตอบ</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Required" xml:space="preserve" approved="no"> + <trans-unit id="Required" xml:space="preserve"> <source>Required</source> - <target state="translated">ที่จำเป็น</target> + <target>ที่จำเป็น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reset" xml:space="preserve" approved="no"> + <trans-unit id="Reset" xml:space="preserve"> <source>Reset</source> - <target state="translated">รีเซ็ต</target> + <target>รีเซ็ต</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reset colors" xml:space="preserve" approved="no"> + <trans-unit id="Reset colors" xml:space="preserve"> <source>Reset colors</source> - <target state="translated">รีเซ็ตสี</target> + <target>รีเซ็ตสี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reset to defaults" xml:space="preserve" approved="no"> + <trans-unit id="Reset to defaults" xml:space="preserve"> <source>Reset to defaults</source> - <target state="translated">รีเซ็ตเป็นค่าเริ่มต้น</target> + <target>รีเซ็ตเป็นค่าเริ่มต้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve" approved="no"> + <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> <source>Restart the app to create a new chat profile</source> - <target state="translated">รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่</target> + <target>รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Restart the app to use imported chat database" xml:space="preserve" approved="no"> + <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> <source>Restart the app to use imported chat database</source> - <target state="translated">รีสตาร์ทแอปเพื่อใช้ฐานข้อมูลการแชทที่นำเข้า</target> + <target>รีสตาร์ทแอปเพื่อใช้ฐานข้อมูลการแชทที่นำเข้า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Restore" xml:space="preserve" approved="no"> + <trans-unit id="Restore" xml:space="preserve"> <source>Restore</source> - <target state="translated">คืนค่า</target> + <target>คืนค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Restore database backup" xml:space="preserve" approved="no"> + <trans-unit id="Restore database backup" xml:space="preserve"> <source>Restore database backup</source> - <target state="translated">คืนค่าการสำรองฐานข้อมูล</target> + <target>คืนค่าการสำรองฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Restore database backup?" xml:space="preserve" approved="no"> + <trans-unit id="Restore database backup?" xml:space="preserve"> <source>Restore database backup?</source> - <target state="translated">คืนค่าฐานข้อมูลสำรองไหม?</target> + <target>คืนค่าฐานข้อมูลสำรองไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Restore database error" xml:space="preserve" approved="no"> + <trans-unit id="Restore database error" xml:space="preserve"> <source>Restore database error</source> - <target state="translated">กู้คืนข้อผิดพลาดของฐานข้อมูล</target> + <target>กู้คืนข้อผิดพลาดของฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Reveal" xml:space="preserve" approved="no"> + <trans-unit id="Reveal" xml:space="preserve"> <source>Reveal</source> - <target state="translated">เปิดเผย</target> + <target>เปิดเผย</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Revert" xml:space="preserve" approved="no"> + <trans-unit id="Revert" xml:space="preserve"> <source>Revert</source> - <target state="translated">เปลี่ยนกลับ</target> + <target>เปลี่ยนกลับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Revoke" xml:space="preserve" approved="no"> + <trans-unit id="Revoke" xml:space="preserve"> <source>Revoke</source> - <target state="translated">ถอน</target> + <target>ถอน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Revoke file" xml:space="preserve" approved="no"> + <trans-unit id="Revoke file" xml:space="preserve"> <source>Revoke file</source> - <target state="translated">ถอนไฟล์</target> + <target>ถอนไฟล์</target> <note>cancel file action</note> </trans-unit> - <trans-unit id="Revoke file?" xml:space="preserve" approved="no"> + <trans-unit id="Revoke file?" xml:space="preserve"> <source>Revoke file?</source> - <target state="translated">ถอนไฟล์?</target> + <target>ถอนไฟล์?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Role" xml:space="preserve" approved="no"> + <trans-unit id="Role" xml:space="preserve"> <source>Role</source> - <target state="translated">บทบาท</target> + <target>บทบาท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Run chat" xml:space="preserve" approved="no"> + <trans-unit id="Run chat" xml:space="preserve"> <source>Run chat</source> - <target state="translated">เรียกใช้แชท</target> + <target>เรียกใช้แชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve" approved="no"> + <trans-unit id="SMP servers" xml:space="preserve"> <source>SMP servers</source> - <target state="translated">เซิร์ฟเวอร์ SMP</target> + <target>เซิร์ฟเวอร์ SMP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save" xml:space="preserve" approved="no"> + <trans-unit id="Save" xml:space="preserve"> <source>Save</source> - <target state="translated">บันทึก</target> + <target>บันทึก</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Save (and notify contacts)" xml:space="preserve" approved="no"> + <trans-unit id="Save (and notify contacts)" xml:space="preserve"> <source>Save (and notify contacts)</source> - <target state="translated">บันทึก (และแจ้งผู้ติดต่อ)</target> + <target>บันทึก (และแจ้งผู้ติดต่อ)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save and notify contact" xml:space="preserve" approved="no"> + <trans-unit id="Save and notify contact" xml:space="preserve"> <source>Save and notify contact</source> - <target state="translated">บันทึกและแจ้งผู้ติดต่อ</target> + <target>บันทึกและแจ้งผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save and notify group members" xml:space="preserve" approved="no"> + <trans-unit id="Save and notify group members" xml:space="preserve"> <source>Save and notify group members</source> - <target state="translated">บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ</target> + <target>บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save and update group profile" xml:space="preserve" approved="no"> + <trans-unit id="Save and update group profile" xml:space="preserve"> <source>Save and update group profile</source> - <target state="translated">บันทึกและอัปเดตโปรไฟล์กลุ่ม</target> + <target>บันทึกและอัปเดตโปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save archive" xml:space="preserve" approved="no"> + <trans-unit id="Save archive" xml:space="preserve"> <source>Save archive</source> - <target state="translated">บันทึกไฟล์เก็บถาวร</target> + <target>บันทึกไฟล์เก็บถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve" approved="no"> + <trans-unit id="Save auto-accept settings" xml:space="preserve"> <source>Save auto-accept settings</source> - <target state="translated">บันทึกการตั้งค่าการยอมรับอัตโนมัติ</target> + <target>บันทึกการตั้งค่าการยอมรับอัตโนมัติ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save group profile" xml:space="preserve" approved="no"> + <trans-unit id="Save group profile" xml:space="preserve"> <source>Save group profile</source> - <target state="translated">บันทึกโปรไฟล์กลุ่ม</target> + <target>บันทึกโปรไฟล์กลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save passphrase and open chat" xml:space="preserve" approved="no"> + <trans-unit id="Save passphrase and open chat" xml:space="preserve"> <source>Save passphrase and open chat</source> - <target state="translated">บันทึกรหัสผ่านและเปิดแชท</target> + <target>บันทึกรหัสผ่านและเปิดแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save passphrase in Keychain" xml:space="preserve" approved="no"> + <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> <source>Save passphrase in Keychain</source> - <target state="translated">บันทึกข้อความรหัสผ่านใน Keychain</target> + <target>บันทึกข้อความรหัสผ่านใน Keychain</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save preferences?" xml:space="preserve" approved="no"> + <trans-unit id="Save preferences?" xml:space="preserve"> <source>Save preferences?</source> - <target state="translated">บันทึกการตั้งค่า?</target> + <target>บันทึกการตั้งค่า?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save profile password" xml:space="preserve" approved="no"> + <trans-unit id="Save profile password" xml:space="preserve"> <source>Save profile password</source> - <target state="translated">บันทึกรหัสผ่านโปรไฟล์</target> + <target>บันทึกรหัสผ่านโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save servers" xml:space="preserve" approved="no"> + <trans-unit id="Save servers" xml:space="preserve"> <source>Save servers</source> - <target state="translated">บันทึกเซิร์ฟเวอร์</target> + <target>บันทึกเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save servers?" xml:space="preserve" approved="no"> + <trans-unit id="Save servers?" xml:space="preserve"> <source>Save servers?</source> - <target state="translated">บันทึกเซิร์ฟเวอร์?</target> + <target>บันทึกเซิร์ฟเวอร์?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve" approved="no"> + <trans-unit id="Save settings?" xml:space="preserve"> <source>Save settings?</source> - <target state="translated">บันทึกการตั้งค่า?</target> + <target>บันทึกการตั้งค่า?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save welcome message?" xml:space="preserve" approved="no"> + <trans-unit id="Save welcome message?" xml:space="preserve"> <source>Save welcome message?</source> - <target state="translated">บันทึกข้อความต้อนรับ?</target> + <target>บันทึกข้อความต้อนรับ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve" approved="no"> + <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> <source>Saved WebRTC ICE servers will be removed</source> - <target state="translated">เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</target> + <target>เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Scan QR code" xml:space="preserve" approved="no"> + <trans-unit id="Scan QR code" xml:space="preserve"> <source>Scan QR code</source> - <target state="translated">สแกนคิวอาร์โค้ด</target> + <target>สแกนคิวอาร์โค้ด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Scan code" xml:space="preserve" approved="no"> + <trans-unit id="Scan code" xml:space="preserve"> <source>Scan code</source> - <target state="translated">สแกนรหัส</target> + <target>สแกนรหัส</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Scan security code from your contact's app." xml:space="preserve" approved="no"> + <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> <source>Scan security code from your contact's app.</source> - <target state="translated">สแกนรหัสความปลอดภัยจากแอปของผู้ติดต่อของคุณ</target> + <target>สแกนรหัสความปลอดภัยจากแอปของผู้ติดต่อของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Scan server QR code" xml:space="preserve" approved="no"> + <trans-unit id="Scan server QR code" xml:space="preserve"> <source>Scan server QR code</source> - <target state="translated">สแกนคิวอาร์โค้ดของเซิร์ฟเวอร์</target> + <target>สแกนคิวอาร์โค้ดของเซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Search" xml:space="preserve" approved="no"> + <trans-unit id="Search" xml:space="preserve"> <source>Search</source> - <target state="translated">ค้นหา</target> + <target>ค้นหา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Secure queue" xml:space="preserve" approved="no"> + <trans-unit id="Secure queue" xml:space="preserve"> <source>Secure queue</source> - <target state="translated">คิวที่ปลอดภัย</target> + <target>คิวที่ปลอดภัย</target> <note>server test step</note> </trans-unit> - <trans-unit id="Security assessment" xml:space="preserve" approved="no"> + <trans-unit id="Security assessment" xml:space="preserve"> <source>Security assessment</source> - <target state="translated">การประเมินความปลอดภัย</target> + <target>การประเมินความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Security code" xml:space="preserve" approved="no"> + <trans-unit id="Security code" xml:space="preserve"> <source>Security code</source> - <target state="translated">รหัสความปลอดภัย</target> + <target>รหัสความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Select" xml:space="preserve" approved="no"> + <trans-unit id="Select" xml:space="preserve"> <source>Select</source> - <target state="translated">เลือก</target> + <target>เลือก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Self-destruct" xml:space="preserve" approved="no"> + <trans-unit id="Self-destruct" xml:space="preserve"> <source>Self-destruct</source> - <target state="translated">ทําลายตัวเอง</target> + <target>ทําลายตัวเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Self-destruct passcode" xml:space="preserve" approved="no"> + <trans-unit id="Self-destruct passcode" xml:space="preserve"> <source>Self-destruct passcode</source> - <target state="translated">รหัสผ่านแบบทําลายตัวเอง</target> + <target>รหัสผ่านแบบทําลายตัวเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Self-destruct passcode changed!" xml:space="preserve" approved="no"> + <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> <source>Self-destruct passcode changed!</source> - <target state="translated">รหัสผ่านแบบทําลายตัวเองเปลี่ยนไป!</target> + <target>รหัสผ่านแบบทําลายตัวเองเปลี่ยนไป!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve" approved="no"> + <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> <source>Self-destruct passcode enabled!</source> - <target state="translated">เปิดใช้งานรหัสผ่านแบบทําลายตัวเองแล้ว!</target> + <target>เปิดใช้งานรหัสผ่านแบบทําลายตัวเองแล้ว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send" xml:space="preserve" approved="no"> + <trans-unit id="Send" xml:space="preserve"> <source>Send</source> - <target state="translated">ส่ง</target> + <target>ส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve" approved="no"> + <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> <source>Send a live message - it will update for the recipient(s) as you type it</source> - <target state="translated">ส่งข้อความสด - มันจะอัปเดตสําหรับผู้รับในขณะที่คุณพิมพ์</target> + <target>ส่งข้อความสด - มันจะอัปเดตสําหรับผู้รับในขณะที่คุณพิมพ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve" approved="no"> + <trans-unit id="Send delivery receipts to" xml:space="preserve"> + <source>Send delivery receipts to</source> + <target>ส่งใบเสร็จรับการจัดส่งข้อความไปที่</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send direct message" xml:space="preserve"> <source>Send direct message</source> - <target state="translated">ส่งข้อความโดยตรง</target> + <target>ส่งข้อความโดยตรง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send disappearing message" xml:space="preserve" approved="no"> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> - <target state="translated">ส่งข้อความแบบที่หายไป</target> + <target>ส่งข้อความแบบที่หายไป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send link previews" xml:space="preserve" approved="no"> + <trans-unit id="Send link previews" xml:space="preserve"> <source>Send link previews</source> - <target state="translated">ส่งตัวอย่างลิงก์</target> + <target>ส่งตัวอย่างลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send live message" xml:space="preserve" approved="no"> + <trans-unit id="Send live message" xml:space="preserve"> <source>Send live message</source> - <target state="translated">ส่งข้อความสด</target> + <target>ส่งข้อความสด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications" xml:space="preserve" approved="no"> + <trans-unit id="Send notifications" xml:space="preserve"> <source>Send notifications</source> - <target state="translated">ส่งการแจ้งเตือน</target> + <target>ส่งการแจ้งเตือน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve" approved="no"> + <trans-unit id="Send notifications:" xml:space="preserve"> <source>Send notifications:</source> - <target state="translated">ส่งการแจ้งเตือน:</target> + <target>ส่งการแจ้งเตือน:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send questions and ideas" xml:space="preserve" approved="no"> + <trans-unit id="Send questions and ideas" xml:space="preserve"> <source>Send questions and ideas</source> - <target state="translated">ส่งคําถามและความคิด</target> + <target>ส่งคําถามและความคิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve" approved="no"> + <trans-unit id="Send receipts" xml:space="preserve"> + <source>Send receipts</source> + <target>ส่งใบเสร็จ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> <source>Send them from gallery or custom keyboards.</source> - <target state="translated">ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง</target> + <target>ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sender cancelled file transfer." xml:space="preserve" approved="no"> + <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> <source>Sender cancelled file transfer.</source> - <target state="translated">ผู้ส่งยกเลิกการโอนไฟล์</target> + <target>ผู้ส่งยกเลิกการโอนไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sender may have deleted the connection request." xml:space="preserve" approved="no"> + <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> <source>Sender may have deleted the connection request.</source> - <target state="translated">ผู้ส่งอาจลบคําขอการเชื่อมต่อ</target> + <target>ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sending file will be stopped." xml:space="preserve" approved="no"> + <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิดในโปรไฟล์แชทที่มองเห็นได้ทั้งหมด</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิด</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending file will be stopped." xml:space="preserve"> <source>Sending file will be stopped.</source> - <target state="translated">การส่งไฟล์จะหยุดลง</target> + <target>การส่งไฟล์จะหยุดลง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sending via" xml:space="preserve" approved="no"> + <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is disabled for %lld contacts</source> + <target>การส่งใบเสร็จถูกปิดใช้งานสำหรับผู้ติดต่อ %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is enabled for %lld contacts</source> + <target>การส่งใบเสร็จถูกเปิดใช้งานสำหรับผู้ติดต่อ %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending via" xml:space="preserve"> <source>Sending via</source> - <target state="translated">ส่งผ่าน</target> + <target>ส่งผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sent at" xml:space="preserve" approved="no"> + <trans-unit id="Sent at" xml:space="preserve"> <source>Sent at</source> - <target state="translated">ส่งเมื่อ</target> + <target>ส่งเมื่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sent at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Sent at: %@" xml:space="preserve"> <source>Sent at: %@</source> - <target state="translated">ส่งเมื่อ: %@</target> + <target>ส่งเมื่อ: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Sent file event" xml:space="preserve" approved="no"> + <trans-unit id="Sent file event" xml:space="preserve"> <source>Sent file event</source> - <target state="translated">เหตุการณ์ไฟล์ที่ส่ง</target> + <target>เหตุการณ์ไฟล์ที่ส่ง</target> <note>notification</note> </trans-unit> - <trans-unit id="Sent message" xml:space="preserve" approved="no"> + <trans-unit id="Sent message" xml:space="preserve"> <source>Sent message</source> - <target state="translated">ข้อความที่ส่งแล้ว</target> + <target>ข้อความที่ส่งแล้ว</target> <note>message info title</note> </trans-unit> - <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve" approved="no"> + <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> <source>Sent messages will be deleted after set time.</source> - <target state="translated">ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด</target> + <target>ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve" approved="no"> + <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> <source>Server requires authorization to create queues, check password</source> - <target state="translated">เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน</target> + <target>เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน</target> <note>server test error</note> </trans-unit> - <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve" approved="no"> + <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> <source>Server requires authorization to upload, check password</source> - <target state="translated">เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน</target> + <target>เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน</target> <note>server test error</note> </trans-unit> - <trans-unit id="Server test failed!" xml:space="preserve" approved="no"> + <trans-unit id="Server test failed!" xml:space="preserve"> <source>Server test failed!</source> - <target state="translated">การทดสอบเซิร์ฟเวอร์ล้มเหลว!</target> + <target>การทดสอบเซิร์ฟเวอร์ล้มเหลว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Servers" xml:space="preserve" approved="no"> + <trans-unit id="Servers" xml:space="preserve"> <source>Servers</source> - <target state="translated">เซิร์ฟเวอร์</target> + <target>เซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set 1 day" xml:space="preserve" approved="no"> + <trans-unit id="Set 1 day" xml:space="preserve"> <source>Set 1 day</source> - <target state="translated">ตั้ง 1 วัน</target> + <target>ตั้ง 1 วัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set contact name…" xml:space="preserve" approved="no"> + <trans-unit id="Set contact name…" xml:space="preserve"> <source>Set contact name…</source> - <target state="translated">ตั้งชื่อผู้ติดต่อ…</target> + <target>ตั้งชื่อผู้ติดต่อ…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set group preferences" xml:space="preserve" approved="no"> + <trans-unit id="Set group preferences" xml:space="preserve"> <source>Set group preferences</source> - <target state="translated">ตั้งค่าการกําหนดลักษณะกลุ่ม</target> + <target>ตั้งค่าการกําหนดลักษณะกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set it instead of system authentication." xml:space="preserve" approved="no"> + <trans-unit id="Set it instead of system authentication." xml:space="preserve"> <source>Set it instead of system authentication.</source> - <target state="translated">ตั้งค่าแทนการรับรองความถูกต้องของระบบ</target> + <target>ตั้งแทนการรับรองความถูกต้องของระบบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set passcode" xml:space="preserve" approved="no"> + <trans-unit id="Set passcode" xml:space="preserve"> <source>Set passcode</source> - <target state="translated">ตั้งรหัสผ่าน</target> + <target>ตั้งรหัสผ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set passphrase to export" xml:space="preserve" approved="no"> + <trans-unit id="Set passphrase to export" xml:space="preserve"> <source>Set passphrase to export</source> - <target state="translated">ตั้งค่ารหัสผ่านเพื่อส่งออก</target> + <target>ตั้งรหัสผ่านเพื่อส่งออก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set the message shown to new members!" xml:space="preserve" approved="no"> + <trans-unit id="Set the message shown to new members!" xml:space="preserve"> <source>Set the message shown to new members!</source> - <target state="translated">ตั้งค่าข้อความที่แสดงต่อสมาชิกใหม่!</target> + <target>ตั้งข้อความที่แสดงต่อสมาชิกใหม่!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve" approved="no"> + <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> <source>Set timeouts for proxy/VPN</source> - <target state="translated">ตั้งค่าการหมดเวลาสำหรับพร็อกซี/VPN</target> + <target>ตั้งค่าการหมดเวลาสำหรับพร็อกซี/VPN</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Settings" xml:space="preserve" approved="no"> + <trans-unit id="Settings" xml:space="preserve"> <source>Settings</source> - <target state="translated">การตั้งค่า</target> + <target>การตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share" xml:space="preserve" approved="no"> + <trans-unit id="Share" xml:space="preserve"> <source>Share</source> - <target state="translated">แชร์</target> + <target>แชร์</target> <note>chat item action</note> </trans-unit> - <trans-unit id="Share 1-time link" xml:space="preserve" approved="no"> + <trans-unit id="Share 1-time link" xml:space="preserve"> <source>Share 1-time link</source> - <target state="translated">แชร์ลิงก์แบบใช้ครั้งเดียว</target> + <target>แชร์ลิงก์แบบใช้ครั้งเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share address" xml:space="preserve" approved="no"> + <trans-unit id="Share address" xml:space="preserve"> <source>Share address</source> - <target state="translated">แชร์ที่อยู่</target> + <target>แชร์ที่อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share address with contacts?" xml:space="preserve" approved="no"> + <trans-unit id="Share address with contacts?" xml:space="preserve"> <source>Share address with contacts?</source> - <target state="translated">แชร์ที่อยู่กับผู้ติดต่อ?</target> + <target>แชร์ที่อยู่กับผู้ติดต่อ?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share link" xml:space="preserve" approved="no"> + <trans-unit id="Share link" xml:space="preserve"> <source>Share link</source> - <target state="translated">แชร์ลิงก์</target> + <target>แชร์ลิงก์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve" approved="no"> + <trans-unit id="Share one-time invitation link" xml:space="preserve"> <source>Share one-time invitation link</source> - <target state="translated">แชร์ลิงก์เชิญแบบใช้ครั้งเดียว</target> + <target>แชร์ลิงก์เชิญแบบใช้ครั้งเดียว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share with contacts" xml:space="preserve" approved="no"> + <trans-unit id="Share with contacts" xml:space="preserve"> <source>Share with contacts</source> - <target state="translated">แชร์กับผู้ติดต่อ</target> + <target>แชร์กับผู้ติดต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show calls in phone history" xml:space="preserve" approved="no"> + <trans-unit id="Show calls in phone history" xml:space="preserve"> <source>Show calls in phone history</source> - <target state="translated">แสดงการโทรในประวัติการโทร</target> + <target>แสดงการโทรในประวัติการโทร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show developer options" xml:space="preserve" approved="no"> + <trans-unit id="Show developer options" xml:space="preserve"> <source>Show developer options</source> - <target state="translated">แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์</target> + <target>แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show preview" xml:space="preserve" approved="no"> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> - <target state="translated">แสดงตัวอย่าง</target> + <target>แสดงตัวอย่าง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show:" xml:space="preserve" approved="no"> + <trans-unit id="Show:" xml:space="preserve"> <source>Show:</source> - <target state="translated">แสดง:</target> + <target>แสดง:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Address" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX Address" xml:space="preserve"> <source>SimpleX Address</source> - <target state="translated">ที่อยู่ SimpleX</target> + <target>ที่อยู่ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve" approved="no"> + <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> <source>SimpleX Chat security was audited by Trail of Bits.</source> - <target state="translated">ความปลอดภัยของ SimpleX Chat ได้รับการตรวจสอบโดย Trail of Bits</target> + <target>ความปลอดภัยของ SimpleX Chat ได้รับการตรวจสอบโดย Trail of Bits</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX Lock" xml:space="preserve"> <source>SimpleX Lock</source> - <target state="translated">ล็อค SimpleX</target> + <target>ล็อค SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock mode" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX Lock mode" xml:space="preserve"> <source>SimpleX Lock mode</source> - <target state="translated">โหมดล็อค SimpleX</target> + <target>โหมดล็อค SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> <source>SimpleX Lock not enabled!</source> - <target state="translated">ไม่ได้เปิดใช้งาน SimpleX Lock!</target> + <target>ไม่ได้เปิดใช้งาน SimpleX Lock!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock turned on" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> <source>SimpleX Lock turned on</source> - <target state="translated">SimpleX Lock เปิดอยู่</target> + <target>SimpleX Lock เปิดอยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX address" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX address" xml:space="preserve"> <source>SimpleX address</source> - <target state="translated">ที่อยู่ SimpleX</target> + <target>ที่อยู่ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX contact address" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX contact address" xml:space="preserve"> <source>SimpleX contact address</source> - <target state="translated">ที่อยู่ติดต่อ SimpleX</target> + <target>ที่อยู่ติดต่อ SimpleX</target> <note>simplex link type</note> </trans-unit> - <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> <source>SimpleX encrypted message or connection event</source> - <target state="translated">ข้อความ encrypt ของ SimpleX หรือเหตุการณ์การเชื่อมต่อ</target> + <target>ข้อความ encrypt ของ SimpleX หรือเหตุการณ์การเชื่อมต่อ</target> <note>notification</note> </trans-unit> - <trans-unit id="SimpleX group link" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX group link" xml:space="preserve"> <source>SimpleX group link</source> - <target state="translated">ลิงค์กลุ่ม SimpleX</target> + <target>ลิงค์กลุ่ม SimpleX</target> <note>simplex link type</note> </trans-unit> - <trans-unit id="SimpleX links" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX links" xml:space="preserve"> <source>SimpleX links</source> - <target state="translated">ลิงก์ SimpleX</target> + <target>ลิงก์ SimpleX</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX one-time invitation" xml:space="preserve" approved="no"> + <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> <source>SimpleX one-time invitation</source> - <target state="translated">คำเชิญ SimpleX แบบครั้งเดียว</target> + <target>คำเชิญ SimpleX แบบครั้งเดียว</target> <note>simplex link type</note> </trans-unit> - <trans-unit id="Skip" xml:space="preserve" approved="no"> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> - <target state="translated">ข้าม</target> + <target>ข้าม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Skipped messages" xml:space="preserve" approved="no"> + <trans-unit id="Skipped messages" xml:space="preserve"> <source>Skipped messages</source> - <target state="translated">ข้อความที่ข้ามไป</target> + <target>ข้อความที่ข้ามไป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Somebody" xml:space="preserve" approved="no"> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> + <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Somebody" xml:space="preserve"> <source>Somebody</source> - <target state="translated">ใครบางคน</target> + <target>ใครบางคน</target> <note>notification title</note> </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve" approved="no"> + <trans-unit id="Start a new chat" xml:space="preserve"> <source>Start a new chat</source> - <target state="translated">เริ่มแชทใหม่</target> + <target>เริ่มแชทใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Start chat" xml:space="preserve" approved="no"> + <trans-unit id="Start chat" xml:space="preserve"> <source>Start chat</source> - <target state="translated">เริ่มแชท</target> + <target>เริ่มแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Start migration" xml:space="preserve" approved="no"> + <trans-unit id="Start migration" xml:space="preserve"> <source>Start migration</source> - <target state="translated">เริ่มการย้ายข้อมูล</target> + <target>เริ่มการย้ายข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop" xml:space="preserve" approved="no"> + <trans-unit id="Stop" xml:space="preserve"> <source>Stop</source> - <target state="translated">หยุด</target> + <target>หยุด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop SimpleX" xml:space="preserve" approved="no"> + <trans-unit id="Stop SimpleX" xml:space="preserve"> <source>Stop SimpleX</source> - <target state="translated">หยุด SimpleX</target> + <target>หยุด SimpleX</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve" approved="no"> + <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> <source>Stop chat to enable database actions</source> - <target state="translated">หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล</target> + <target>หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve" approved="no"> + <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> - <target state="translated">หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง</target> + <target>หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop chat?" xml:space="preserve" approved="no"> + <trans-unit id="Stop chat?" xml:space="preserve"> <source>Stop chat?</source> - <target state="translated">หยุดแชท?</target> + <target>หยุดแชท?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop file" xml:space="preserve" approved="no"> + <trans-unit id="Stop file" xml:space="preserve"> <source>Stop file</source> - <target state="translated">หยุดไฟล์</target> + <target>หยุดไฟล์</target> <note>cancel file action</note> </trans-unit> - <trans-unit id="Stop receiving file?" xml:space="preserve" approved="no"> + <trans-unit id="Stop receiving file?" xml:space="preserve"> <source>Stop receiving file?</source> - <target state="translated">หยุดรับไฟล์?</target> + <target>หยุดรับไฟล์?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop sending file?" xml:space="preserve" approved="no"> + <trans-unit id="Stop sending file?" xml:space="preserve"> <source>Stop sending file?</source> - <target state="translated">หยุดส่งไฟล์ไหม?</target> + <target>หยุดส่งไฟล์ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop sharing" xml:space="preserve" approved="no"> + <trans-unit id="Stop sharing" xml:space="preserve"> <source>Stop sharing</source> - <target state="translated">หยุดแชร์</target> + <target>หยุดแชร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop sharing address?" xml:space="preserve" approved="no"> + <trans-unit id="Stop sharing address?" xml:space="preserve"> <source>Stop sharing address?</source> - <target state="translated">หยุดแชร์ที่อยู่ไหม?</target> + <target>หยุดแชร์ที่อยู่ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Submit" xml:space="preserve" approved="no"> + <trans-unit id="Submit" xml:space="preserve"> <source>Submit</source> - <target state="translated">ส่ง</target> + <target>ส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Support SimpleX Chat" xml:space="preserve" approved="no"> + <trans-unit id="Support SimpleX Chat" xml:space="preserve"> <source>Support SimpleX Chat</source> - <target state="translated">สนับสนุนแชท SimpleX</target> + <target>สนับสนุน SimpleX แชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="System" xml:space="preserve" approved="no"> + <trans-unit id="System" xml:space="preserve"> <source>System</source> - <target state="translated">ระบบ</target> + <target>ระบบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="System authentication" xml:space="preserve" approved="no"> + <trans-unit id="System authentication" xml:space="preserve"> <source>System authentication</source> - <target state="translated">การรับรองความถูกต้องของระบบ</target> + <target>การรับรองความถูกต้องของระบบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="TCP connection timeout" xml:space="preserve" approved="no"> + <trans-unit id="TCP connection timeout" xml:space="preserve"> <source>TCP connection timeout</source> - <target state="translated">หมดเวลาการเชื่อมต่อ TCP</target> + <target>หมดเวลาการเชื่อมต่อ TCP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="TCP_KEEPCNT" xml:space="preserve" approved="no"> + <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> <source>TCP_KEEPCNT</source> - <target state="translated">TCP_KEEPCNT</target> + <target>TCP_KEEPCNT</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="TCP_KEEPIDLE" xml:space="preserve" approved="no"> + <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> <source>TCP_KEEPIDLE</source> - <target state="translated">TCP_KEEPIDLE</target> + <target>TCP_KEEPIDLE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="TCP_KEEPINTVL" xml:space="preserve" approved="no"> + <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> <source>TCP_KEEPINTVL</source> - <target state="translated">TCP_KEEPINTVL</target> + <target>TCP_KEEPINTVL</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Take picture" xml:space="preserve" approved="no"> + <trans-unit id="Take picture" xml:space="preserve"> <source>Take picture</source> - <target state="translated">ถ่ายภาพ</target> + <target>ถ่ายภาพ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap button " xml:space="preserve" approved="no"> + <trans-unit id="Tap button " xml:space="preserve"> <source>Tap button </source> - <target state="translated">แตะปุ่ม </target> + <target>แตะปุ่ม </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to activate profile." xml:space="preserve" approved="no"> + <trans-unit id="Tap to activate profile." xml:space="preserve"> <source>Tap to activate profile.</source> - <target state="translated">แตะเพื่อเปิดใช้งานโปรไฟล์</target> + <target>แตะเพื่อเปิดใช้งานโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to join" xml:space="preserve" approved="no"> + <trans-unit id="Tap to join" xml:space="preserve"> <source>Tap to join</source> - <target state="translated">แตะเพื่อเข้าร่วม</target> + <target>แตะเพื่อเข้าร่วม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to join incognito" xml:space="preserve" approved="no"> + <trans-unit id="Tap to join incognito" xml:space="preserve"> <source>Tap to join incognito</source> - <target state="translated">แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน</target> + <target>แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve" approved="no"> + <trans-unit id="Tap to start a new chat" xml:space="preserve"> <source>Tap to start a new chat</source> - <target state="translated">แตะเพื่อเริ่มแชทใหม่</target> + <target>แตะเพื่อเริ่มแชทใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Test failed at step %@." xml:space="preserve" approved="no"> + <trans-unit id="Test failed at step %@." xml:space="preserve"> <source>Test failed at step %@.</source> - <target state="translated">การทดสอบล้มเหลวในขั้นตอน %@</target> + <target>การทดสอบล้มเหลวในขั้นตอน %@</target> <note>server test failure</note> </trans-unit> - <trans-unit id="Test server" xml:space="preserve" approved="no"> + <trans-unit id="Test server" xml:space="preserve"> <source>Test server</source> - <target state="translated">เซิร์ฟเวอร์ทดสอบ</target> + <target>เซิร์ฟเวอร์ทดสอบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Test servers" xml:space="preserve" approved="no"> + <trans-unit id="Test servers" xml:space="preserve"> <source>Test servers</source> - <target state="translated">เซิร์ฟเวอร์ทดสอบ</target> + <target>เซิร์ฟเวอร์ทดสอบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tests failed!" xml:space="preserve" approved="no"> + <trans-unit id="Tests failed!" xml:space="preserve"> <source>Tests failed!</source> - <target state="translated">การทดสอบล้มเหลว!</target> + <target>การทดสอบล้มเหลว!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve" approved="no"> + <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> <source>Thank you for installing SimpleX Chat!</source> - <target state="translated">ขอบคุณสำหรับการติดตั้ง SimpleX Chat!</target> + <target>ขอบคุณสำหรับการติดตั้ง SimpleX Chat!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no"> + <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> - <target state="translated">ขอบคุณผู้ใช้ – [มีส่วนร่วมผ่าน Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <target>ขอบคุณผู้ใช้ – [มีส่วนร่วมผ่าน Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve" approved="no"> + <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> <source>Thanks to the users – contribute via Weblate!</source> - <target state="translated">ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</target> + <target>ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve" approved="no"> + <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> <source>The 1st platform without any user identifiers – private by design.</source> - <target state="translated">แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว</target> + <target>แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve" approved="no"> + <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> - <target state="translated">ID ของข้อความถัดไปไม่ถูกต้อง + <target>ID ของข้อความถัดไปไม่ถูกต้อง (น้อยกว่าหรือเท่ากับข้อความก่อนหน้า) อาจเกิดขึ้นได้เนื่องจากข้อบกพร่องบางอย่างหรือเมื่อการเชื่อมต่อถูกบุกรุก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve" approved="no"> + <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> - <target state="translated">แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน</target> + <target>แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve" approved="no"> + <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> <source>The attempt to change database passphrase was not completed.</source> - <target state="translated">ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์</target> + <target>ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve" approved="no"> + <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> <source>The connection you accepted will be cancelled!</source> - <target state="translated">การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!</target> + <target>การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve" approved="no"> + <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> <source>The contact you shared this link with will NOT be able to connect!</source> - <target state="translated">ผู้ติดต่อที่คุณแชร์ลิงก์นี้ด้วยจะไม่สามารถเชื่อมต่อได้!</target> + <target>ผู้ติดต่อที่คุณแชร์ลิงก์นี้ด้วยจะไม่สามารถเชื่อมต่อได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve" approved="no"> + <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> <source>The created archive is available via app Settings / Database / Old database archive.</source> - <target state="translated">ไฟล์เก็บที่สร้างขึ้นมีอยู่ในการตั้งค่าแอพ / ฐานข้อมูล / คลังฐานข้อมูลเก่า</target> + <target>ไฟล์เก็บที่สร้างขึ้นมีอยู่ในการตั้งค่าแอพ / ฐานข้อมูล / คลังฐานข้อมูลเก่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve" approved="no"> + <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> + <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> <source>The group is fully decentralized – it is visible only to the members.</source> - <target state="translated">กลุ่มมีการกระจายอำนาจอย่างเต็มที่ – มองเห็นได้เฉพาะสมาชิกเท่านั้น</target> + <target>กลุ่มมีการกระจายอำนาจอย่างเต็มที่ – มองเห็นได้เฉพาะสมาชิกเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The hash of the previous message is different." xml:space="preserve" approved="no"> + <trans-unit id="The hash of the previous message is different." xml:space="preserve"> <source>The hash of the previous message is different.</source> - <target state="translated">แฮชของข้อความก่อนหน้านี้แตกต่างกัน</target> + <target>แฮชของข้อความก่อนหน้านี้แตกต่างกัน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The message will be deleted for all members." xml:space="preserve" approved="no"> + <trans-unit id="The message will be deleted for all members." xml:space="preserve"> <source>The message will be deleted for all members.</source> - <target state="translated">ข้อความจะถูกลบสำหรับสมาชิกทั้งหมด</target> + <target>ข้อความจะถูกลบสำหรับสมาชิกทั้งหมด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve" approved="no"> + <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> <source>The message will be marked as moderated for all members.</source> - <target state="translated">ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน</target> + <target>ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve" approved="no"> + <trans-unit id="The next generation of private messaging" xml:space="preserve"> <source>The next generation of private messaging</source> - <target state="translated">การส่งข้อความส่วนตัวรุ่นต่อไป</target> + <target>การส่งข้อความส่วนตัวรุ่นต่อไป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve" approved="no"> + <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> <source>The old database was not removed during the migration, it can be deleted.</source> - <target state="translated">ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้</target> + <target>ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve" approved="no"> + <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> <source>The profile is only shared with your contacts.</source> - <target state="translated">โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น</target> + <target>โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The sender will NOT be notified" xml:space="preserve" approved="no"> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> + <source>The second tick we missed! ✅</source> + <target>ขีดที่สองที่เราพลาด! ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The sender will NOT be notified" xml:space="preserve"> <source>The sender will NOT be notified</source> - <target state="translated">ผู้ส่งจะไม่ได้รับแจ้ง</target> + <target>ผู้ส่งจะไม่ได้รับแจ้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve" approved="no"> + <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> <source>The servers for new connections of your current chat profile **%@**.</source> - <target state="translated">เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**</target> + <target>เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Theme" xml:space="preserve" approved="no"> + <trans-unit id="Theme" xml:space="preserve"> <source>Theme</source> - <target state="translated">ธีม</target> + <target>ธีม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve" approved="no"> + <trans-unit id="There should be at least one user profile." xml:space="preserve"> <source>There should be at least one user profile.</source> - <target state="translated">ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์</target> + <target>ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve" approved="no"> + <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> <source>There should be at least one visible user profile.</source> - <target state="translated">ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้</target> + <target>ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve" approved="no"> + <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> + <source>These settings are for your current profile **%@**.</source> + <target>การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@**</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <target state="translated">การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</target> + <target>การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve" approved="no"> + <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <target state="translated">การดำเนินการนี้ไม่สามารถเลิกทำได้ - ข้อความที่ส่งและรับก่อนหน้าที่เลือกไว้จะถูกลบ อาจใช้เวลาหลายนาที</target> + <target>การดำเนินการนี้ไม่สามารถเลิกทำได้ - ข้อความที่ส่งและรับก่อนหน้าที่เลือกไว้จะถูกลบ อาจใช้เวลาหลายนาที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve" approved="no"> + <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <target state="translated">การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target> + <target>การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This error is permanent for this connection, please re-connect." xml:space="preserve" approved="no"> - <source>This error is permanent for this connection, please re-connect.</source> - <target state="translated">ข้อผิดพลาดนี้เกิดขึ้นอย่างถาวรสำหรับการเชื่อมต่อนี้ โปรดเชื่อมต่อใหม่</target> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve"> - <source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This group no longer exists." xml:space="preserve" approved="no"> + <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> - <target state="translated">ไม่มีกลุ่มนี้แล้ว</target> + <target>ไม่มีกลุ่มนี้แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve" approved="no"> + <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> <source>This setting applies to messages in your current chat profile **%@**.</source> - <target state="translated">การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@**</target> + <target>การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve" approved="no"> + <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> <source>To ask any questions and to receive updates:</source> - <target state="translated">หากต้องการถามคำถามและรับการอัปเดต:</target> + <target>หากต้องการถามคำถามและรับการอัปเดต:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve" approved="no"> + <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> <source>To connect, your contact can scan QR code or use the link in the app.</source> - <target state="translated">เพื่อการเชื่อมต่อ ผู้ติดต่อของคุณสามารถสแกนคิวอาร์โค้ดหรือใช้ลิงก์ในแอป</target> + <target>เพื่อการเชื่อมต่อ ผู้ติดต่อของคุณสามารถสแกนคิวอาร์โค้ดหรือใช้ลิงก์ในแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve" approved="no"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target state="translated">หากต้องการค้นหาโปรไฟล์ที่ใช้สำหรับการเชื่อมต่อแบบไม่ระบุตัวตน ให้แตะชื่อผู้ติดต่อหรือชื่อกลุ่มที่ด้านบนของแชท</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To make a new connection" xml:space="preserve" approved="no"> + <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> - <target state="translated">เพื่อสร้างการเชื่อมต่อใหม่</target> + <target>เพื่อสร้างการเชื่อมต่อใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve" approved="no"> + <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target state="translated">เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย</target> + <target>เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve" approved="no"> + <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> <source>To protect timezone, image/voice files use UTC.</source> - <target state="translated">ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา</target> + <target>ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve" approved="no"> + <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> <source>To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled.</source> - <target state="translated">เพื่อปกป้องข้อมูลของคุณ ให้เปิด SimpleX Lock + <target>เพื่อปกป้องข้อมูลของคุณ ให้เปิด SimpleX Lock คุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve" approved="no"> + <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> <source>To record voice message please grant permission to use Microphone.</source> - <target state="translated">ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน</target> + <target>ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve" approved="no"> + <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> - <target state="translated">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ**</target> + <target>หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve" approved="no"> + <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> <source>To support instant push notifications the chat database has to be migrated.</source> - <target state="translated">เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย</target> + <target>เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve" approved="no"> + <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> - <target state="translated">ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ</target> + <target>ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Transport isolation" xml:space="preserve" approved="no"> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> - <target state="translated">การแยกการขนส่ง</target> + <target>การแยกการขนส่ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve" approved="no"> + <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> - <target state="translated">กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@)</target> + <target>กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve" approved="no"> + <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> <source>Trying to connect to the server used to receive messages from this contact.</source> - <target state="translated">พยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target> + <target>พยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off" xml:space="preserve" approved="no"> + <trans-unit id="Turn off" xml:space="preserve"> <source>Turn off</source> - <target state="translated">ปิด</target> + <target>ปิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve" approved="no"> + <trans-unit id="Turn off notifications?" xml:space="preserve"> <source>Turn off notifications?</source> - <target state="translated">ปิดการแจ้งเตือนไหม?</target> + <target>ปิดการแจ้งเตือนไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Turn on" xml:space="preserve" approved="no"> + <trans-unit id="Turn on" xml:space="preserve"> <source>Turn on</source> - <target state="translated">เปิด</target> + <target>เปิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unable to record voice message" xml:space="preserve" approved="no"> + <trans-unit id="Unable to record voice message" xml:space="preserve"> <source>Unable to record voice message</source> - <target state="translated">ไม่สามารถบันทึกข้อความเสียง</target> + <target>ไม่สามารถบันทึกข้อความเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve" approved="no"> + <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> - <target state="translated">ข้อผิดพลาดที่ไม่คาดคิด: %@</target> - <note>No comment provided by engineer.</note> + <target>ข้อผิดพลาดที่ไม่คาดคิด: %@</target> + <note>item status description</note> </trans-unit> - <trans-unit id="Unexpected migration state" xml:space="preserve" approved="no"> + <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> - <target state="translated">สถานะการย้ายข้อมูลที่ไม่คาดคิด</target> + <target>สถานะการย้ายข้อมูลที่ไม่คาดคิด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unhide" xml:space="preserve" approved="no"> + <trans-unit id="Unfav." xml:space="preserve"> + <source>Unfav.</source> + <target>เลิกชอบ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide" xml:space="preserve"> <source>Unhide</source> - <target state="translated">ยกเลิกการซ่อน</target> + <target>ยกเลิกการซ่อน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unhide chat profile" xml:space="preserve" approved="no"> + <trans-unit id="Unhide chat profile" xml:space="preserve"> <source>Unhide chat profile</source> - <target state="translated">ยกเลิกการซ่อนโปรไฟล์การแชท</target> + <target>ยกเลิกการซ่อนโปรไฟล์การแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unhide profile" xml:space="preserve" approved="no"> + <trans-unit id="Unhide profile" xml:space="preserve"> <source>Unhide profile</source> - <target state="translated">เลิกซ่อนโปรไฟล์</target> + <target>เลิกซ่อนโปรไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unit" xml:space="preserve" approved="no"> + <trans-unit id="Unit" xml:space="preserve"> <source>Unit</source> - <target state="translated">หน่วย</target> + <target>หน่วย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unknown caller" xml:space="preserve" approved="no"> + <trans-unit id="Unknown caller" xml:space="preserve"> <source>Unknown caller</source> - <target state="translated">ผู้โทรที่ไม่รู้จัก</target> + <target>ผู้โทรที่ไม่รู้จัก</target> <note>callkit banner</note> </trans-unit> - <trans-unit id="Unknown database error: %@" xml:space="preserve" approved="no"> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> <source>Unknown database error: %@</source> - <target state="translated">ข้อผิดพลาดของฐานข้อมูลที่ไม่รู้จัก: %@</target> + <target>ข้อผิดพลาดของฐานข้อมูลที่ไม่รู้จัก: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unknown error" xml:space="preserve" approved="no"> + <trans-unit id="Unknown error" xml:space="preserve"> <source>Unknown error</source> - <target state="translated">ข้อผิดพลาดที่ไม่รู้จัก</target> + <target>ข้อผิดพลาดที่ไม่รู้จัก</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve" approved="no"> + <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> - <target state="translated">ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน</target> + <target>ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve" approved="no"> + <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> - <target state="translated">เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน + <target>เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน ในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unlock" xml:space="preserve" approved="no"> + <trans-unit id="Unlock" xml:space="preserve"> <source>Unlock</source> - <target state="translated">ปลดล็อค</target> + <target>ปลดล็อค</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unlock app" xml:space="preserve" approved="no"> + <trans-unit id="Unlock app" xml:space="preserve"> <source>Unlock app</source> - <target state="translated">ปลดล็อคแอป</target> + <target>ปลดล็อคแอป</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Unmute" xml:space="preserve" approved="no"> + <trans-unit id="Unmute" xml:space="preserve"> <source>Unmute</source> - <target state="translated">เปิดเสียง</target> + <target>เปิดเสียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unread" xml:space="preserve" approved="no"> + <trans-unit id="Unread" xml:space="preserve"> <source>Unread</source> - <target state="translated">เปลี่ยนเป็นยังไม่ได้อ่าน</target> + <target>เปลี่ยนเป็นยังไม่ได้อ่าน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update" xml:space="preserve" approved="no"> + <trans-unit id="Update" xml:space="preserve"> <source>Update</source> - <target state="translated">อัปเดต</target> + <target>อัปเดต</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve" approved="no"> + <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> <source>Update .onion hosts setting?</source> - <target state="translated">อัปเดตการตั้งค่าโฮสต์ .onion ไหม?</target> + <target>อัปเดตการตั้งค่าโฮสต์ .onion ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update database passphrase" xml:space="preserve" approved="no"> + <trans-unit id="Update database passphrase" xml:space="preserve"> <source>Update database passphrase</source> - <target state="translated">อัปเดตรหัสผ่านของฐานข้อมูล</target> + <target>อัปเดตรหัสผ่านของฐานข้อมูล</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update network settings?" xml:space="preserve" approved="no"> + <trans-unit id="Update network settings?" xml:space="preserve"> <source>Update network settings?</source> - <target state="translated">อัปเดตการตั้งค่าเครือข่ายไหม?</target> + <target>อัปเดตการตั้งค่าเครือข่ายไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve" approved="no"> + <trans-unit id="Update transport isolation mode?" xml:space="preserve"> <source>Update transport isolation mode?</source> - <target state="translated">อัปเดตโหมดการแยกการขนส่งไหม?</target> + <target>อัปเดตโหมดการแยกการขนส่งไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve" approved="no"> + <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> <source>Updating settings will re-connect the client to all servers.</source> - <target state="translated">การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</target> + <target>การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve" approved="no"> + <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> <source>Updating this setting will re-connect the client to all servers.</source> - <target state="translated">การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</target> + <target>การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Upgrade and open chat" xml:space="preserve" approved="no"> + <trans-unit id="Upgrade and open chat" xml:space="preserve"> <source>Upgrade and open chat</source> - <target state="translated">อัปเกรดและเปิดการแชท</target> + <target>อัปเกรดและเปิดการแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Upload file" xml:space="preserve" approved="no"> + <trans-unit id="Upload file" xml:space="preserve"> <source>Upload file</source> - <target state="translated">อัปโหลดไฟล์</target> + <target>อัปโหลดไฟล์</target> <note>server test step</note> </trans-unit> - <trans-unit id="Use .onion hosts" xml:space="preserve" approved="no"> + <trans-unit id="Use .onion hosts" xml:space="preserve"> <source>Use .onion hosts</source> - <target state="translated">ใช้โฮสต์ .onion</target> + <target>ใช้โฮสต์ .onion</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve" approved="no"> + <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> <source>Use SimpleX Chat servers?</source> - <target state="translated">ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม?</target> + <target>ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Use chat" xml:space="preserve" approved="no"> + <trans-unit id="Use chat" xml:space="preserve"> <source>Use chat</source> - <target state="translated">ใช้แชท</target> + <target>ใช้แชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Use for new connections" xml:space="preserve" approved="no"> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> - <target state="translated">ใช้สำหรับการเชื่อมต่อใหม่</target> + <target>ใช้สำหรับการเชื่อมต่อใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Use iOS call interface" xml:space="preserve" approved="no"> + <trans-unit id="Use iOS call interface" xml:space="preserve"> <source>Use iOS call interface</source> - <target state="translated">ใช้อินเทอร์เฟซการโทร iOS</target> + <target>ใช้อินเทอร์เฟซการโทร iOS</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Use server" xml:space="preserve" approved="no"> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> - <target state="translated">ใช้เซิร์ฟเวอร์</target> + <target>ใช้เซิร์ฟเวอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="User profile" xml:space="preserve" approved="no"> + <trans-unit id="User profile" xml:space="preserve"> <source>User profile</source> - <target state="translated">โปรไฟล์ผู้ใช้</target> + <target>โปรไฟล์ผู้ใช้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve" approved="no"> + <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> <source>Using .onion hosts requires compatible VPN provider.</source> - <target state="translated">การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้</target> + <target>การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Using SimpleX Chat servers." xml:space="preserve" approved="no"> + <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> <source>Using SimpleX Chat servers.</source> - <target state="translated">กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่</target> + <target>กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Verify connection security" xml:space="preserve" approved="no"> + <trans-unit id="Verify connection security" xml:space="preserve"> <source>Verify connection security</source> - <target state="translated">ตรวจสอบความปลอดภัยในการเชื่อมต่อ</target> + <target>ตรวจสอบความปลอดภัยในการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Verify security code" xml:space="preserve" approved="no"> + <trans-unit id="Verify security code" xml:space="preserve"> <source>Verify security code</source> - <target state="translated">ตรวจสอบรหัสความปลอดภัย</target> + <target>ตรวจสอบรหัสความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Via browser" xml:space="preserve" approved="no"> + <trans-unit id="Via browser" xml:space="preserve"> <source>Via browser</source> - <target state="translated">ผ่านเบราว์เซอร์</target> + <target>ผ่านเบราว์เซอร์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Video call" xml:space="preserve" approved="no"> + <trans-unit id="Video call" xml:space="preserve"> <source>Video call</source> - <target state="translated">การสนทนาทางวิดีโอ</target> + <target>การสนทนาทางวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve" approved="no"> + <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> <source>Video will be received when your contact completes uploading it.</source> - <target state="translated">จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จ</target> + <target>จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve" approved="no"> + <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> <source>Video will be received when your contact is online, please wait or check later!</source> - <target state="translated">จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</target> + <target>จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Videos and files up to 1gb" xml:space="preserve" approved="no"> + <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> <source>Videos and files up to 1gb</source> - <target state="translated">วิดีโอและไฟล์สูงสุด 1gb</target> + <target>วิดีโอและไฟล์สูงสุด 1gb</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="View security code" xml:space="preserve" approved="no"> + <trans-unit id="View security code" xml:space="preserve"> <source>View security code</source> - <target state="translated">ดูรหัสความปลอดภัย</target> + <target>ดูรหัสความปลอดภัย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages" xml:space="preserve" approved="no"> + <trans-unit id="Voice messages" xml:space="preserve"> <source>Voice messages</source> - <target state="translated">ข้อความเสียง</target> + <target>ข้อความเสียง</target> <note>chat feature</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve" approved="no"> + <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> <source>Voice messages are prohibited in this chat.</source> - <target state="translated">ห้ามส่งข้อความเสียงในแชทนี้</target> + <target>ห้ามส่งข้อความเสียงในแชทนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve" approved="no"> + <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> <source>Voice messages are prohibited in this group.</source> - <target state="translated">ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้</target> + <target>ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice messages prohibited!" xml:space="preserve" approved="no"> + <trans-unit id="Voice messages prohibited!" xml:space="preserve"> <source>Voice messages prohibited!</source> - <target state="translated">ห้ามข้อความเสียง!</target> + <target>ห้ามข้อความเสียง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Voice message…" xml:space="preserve" approved="no"> + <trans-unit id="Voice message…" xml:space="preserve"> <source>Voice message…</source> - <target state="translated">ข้อความเสียง…</target> + <target>ข้อความเสียง…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Waiting for file" xml:space="preserve" approved="no"> + <trans-unit id="Waiting for file" xml:space="preserve"> <source>Waiting for file</source> - <target state="translated">กำลังรอไฟล์</target> + <target>กำลังรอไฟล์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Waiting for image" xml:space="preserve" approved="no"> + <trans-unit id="Waiting for image" xml:space="preserve"> <source>Waiting for image</source> - <target state="translated">กําลังรอภาพ</target> + <target>กําลังรอภาพ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Waiting for video" xml:space="preserve" approved="no"> + <trans-unit id="Waiting for video" xml:space="preserve"> <source>Waiting for video</source> - <target state="translated">กําลังรอวิดีโอ</target> + <target>กําลังรอวิดีโอ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Warning: you may lose some data!" xml:space="preserve" approved="no"> + <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> <source>Warning: you may lose some data!</source> - <target state="translated">คำเตือน: คุณอาจสูญเสียข้อมูลบางส่วน!</target> + <target>คำเตือน: คุณอาจสูญเสียข้อมูลบางส่วน!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="WebRTC ICE servers" xml:space="preserve" approved="no"> + <trans-unit id="WebRTC ICE servers" xml:space="preserve"> <source>WebRTC ICE servers</source> - <target state="translated">เซิร์ฟเวอร์ WebRTC ICE</target> + <target>เซิร์ฟเวอร์ WebRTC ICE</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Welcome %@!" xml:space="preserve" approved="no"> + <trans-unit id="Welcome %@!" xml:space="preserve"> <source>Welcome %@!</source> - <target state="translated">ยินดีต้อนรับ %@!</target> + <target>ยินดีต้อนรับ %@!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Welcome message" xml:space="preserve" approved="no"> + <trans-unit id="Welcome message" xml:space="preserve"> <source>Welcome message</source> - <target state="translated">ข้อความต้อนรับ</target> + <target>ข้อความต้อนรับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="What's new" xml:space="preserve" approved="no"> + <trans-unit id="What's new" xml:space="preserve"> <source>What's new</source> - <target state="translated">มีอะไรใหม่</target> + <target>มีอะไรใหม่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When available" xml:space="preserve" approved="no"> + <trans-unit id="When available" xml:space="preserve"> <source>When available</source> - <target state="translated">เมื่อพร้อมใช้งาน</target> + <target>เมื่อพร้อมใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve" approved="no"> + <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> <source>When people request to connect, you can accept or reject it.</source> - <target state="translated">เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้</target> + <target>เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve" approved="no"> + <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> - <target state="translated">เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</target> + <target>เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="With optional welcome message." xml:space="preserve" approved="no"> + <trans-unit id="With optional welcome message." xml:space="preserve"> <source>With optional welcome message.</source> - <target state="translated">พร้อมข้อความต้อนรับเพิ่มเติม</target> + <target>พร้อมข้อความต้อนรับที่ไม่บังคับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Wrong database passphrase" xml:space="preserve" approved="no"> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> <source>Wrong database passphrase</source> - <target state="translated">รหัสผ่านฐานข้อมูลไม่ถูกต้อง</target> + <target>รหัสผ่านฐานข้อมูลไม่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Wrong passphrase!" xml:space="preserve" approved="no"> + <trans-unit id="Wrong passphrase!" xml:space="preserve"> <source>Wrong passphrase!</source> - <target state="translated">รหัสผ่านผิด!</target> + <target>รหัสผ่านผิด!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="XFTP servers" xml:space="preserve" approved="no"> + <trans-unit id="XFTP servers" xml:space="preserve"> <source>XFTP servers</source> - <target state="translated">เซิร์ฟเวอร์ XFTP</target> + <target>เซิร์ฟเวอร์ XFTP</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You" xml:space="preserve" approved="no"> + <trans-unit id="You" xml:space="preserve"> <source>You</source> - <target state="translated">คุณ</target> + <target>คุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You accepted connection" xml:space="preserve" approved="no"> + <trans-unit id="You accepted connection" xml:space="preserve"> <source>You accepted connection</source> - <target state="translated">คุณยอมรับการเชื่อมต่อ</target> + <target>คุณยอมรับการเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You allow" xml:space="preserve" approved="no"> + <trans-unit id="You allow" xml:space="preserve"> <source>You allow</source> - <target state="translated">คุณอนุญาต</target> + <target>คุณอนุญาต</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve" approved="no"> + <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> <source>You already have a chat profile with the same display name. Please choose another name.</source> - <target state="translated">คุณมีโปรไฟล์แชทที่ใช้ชื่อแสดงเดียวกันอยู่แล้ว กรุณาเลือกชื่ออื่น</target> + <target>คุณมีโปรไฟล์แชทที่ใช้ชื่อแสดงเดียวกันอยู่แล้ว กรุณาเลือกชื่ออื่น</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You are already connected to %@." xml:space="preserve" approved="no"> + <trans-unit id="You are already connected to %@." xml:space="preserve"> <source>You are already connected to %@.</source> - <target state="translated">คุณได้เชื่อมต่อกับ %@ แล้ว</target> + <target>คุณได้เชื่อมต่อกับ %@ แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve" approved="no"> + <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> <source>You are connected to the server used to receive messages from this contact.</source> - <target state="translated">คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target> + <target>คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You are invited to group" xml:space="preserve" approved="no"> + <trans-unit id="You are invited to group" xml:space="preserve"> <source>You are invited to group</source> - <target state="translated">คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target> + <target>คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve" approved="no"> + <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> <source>You can accept calls from lock screen, without device and app authentication.</source> - <target state="translated">คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป</target> + <target>คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve" approved="no"> + <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <target state="translated">คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**</target> + <target>คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can create it later" xml:space="preserve" approved="no"> + <trans-unit id="You can create it later" xml:space="preserve"> <source>You can create it later</source> - <target state="translated">คุณสามารถสร้างได้ในภายหลัง</target> + <target>คุณสามารถสร้างได้ในภายหลัง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can hide or mute a user profile - swipe it to the right. SimpleX Lock must be enabled." xml:space="preserve"> - <source>You can hide or mute a user profile - swipe it to the right. -SimpleX Lock must be enabled.</source> + <trans-unit id="You can enable later via Settings" xml:space="preserve"> + <source>You can enable later via Settings</source> + <target>คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve" approved="no"> + <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> + <source>You can enable them later via app Privacy & Security settings.</source> + <target>คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> + <source>You can hide or mute a user profile - swipe it to the right.</source> + <target>คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now send messages to %@" xml:space="preserve"> <source>You can now send messages to %@</source> - <target state="translated">ตอนนี้คุณสามารถส่งข้อความถึง %@</target> + <target>ตอนนี้คุณสามารถส่งข้อความถึง %@</target> <note>notification body</note> </trans-unit> - <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve" approved="no"> + <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> <source>You can set lock screen notification preview via settings.</source> - <target state="translated">คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า</target> + <target>คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve" approved="no"> + <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> - <target state="translated">คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</target> + <target>คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve" approved="no"> + <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> <source>You can share this address with your contacts to let them connect with **%@**.</source> - <target state="translated">คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@**</target> + <target>คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@**</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve" approved="no"> + <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> <source>You can share your address as a link or QR code - anybody can connect to you.</source> - <target state="translated">คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้</target> + <target>คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve" approved="no"> + <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> <source>You can start chat via app Settings / Database or by restarting the app</source> - <target state="translated">คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป</target> + <target>คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve" approved="no"> + <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> <source>You can turn on SimpleX Lock via Settings.</source> - <target state="translated">คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า</target> + <target>คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can use markdown to format messages:" xml:space="preserve" approved="no"> + <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> <source>You can use markdown to format messages:</source> - <target state="translated">คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:</target> + <target>คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You can't send messages!" xml:space="preserve" approved="no"> + <trans-unit id="You can't send messages!" xml:space="preserve"> <source>You can't send messages!</source> - <target state="translated">คุณไม่สามารถส่งข้อความได้!</target> + <target>คุณไม่สามารถส่งข้อความได้!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve" approved="no"> + <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <target state="translated">คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา</target> + <target>คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You could not be verified; please try again." xml:space="preserve" approved="no"> + <trans-unit id="You could not be verified; please try again." xml:space="preserve"> <source>You could not be verified; please try again.</source> - <target state="translated">เราไม่สามารถยืนยันคุณได้ กรุณาลองอีกครั้ง.</target> + <target>เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve" approved="no"> + <trans-unit id="You have no chats" xml:space="preserve"> <source>You have no chats</source> - <target state="translated">คุณไม่มีการแชท</target> + <target>คุณไม่มีการแชท</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve" approved="no"> + <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> - <target state="translated">คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์</target> + <target>คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve" approved="no"> - <source>You invited your contact</source> - <target state="translated">คุณได้เชิญผู้ติดต่อของคุณ</target> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You joined this group" xml:space="preserve" approved="no"> + <trans-unit id="You joined this group" xml:space="preserve"> <source>You joined this group</source> - <target state="translated">คุณเข้าร่วมกลุ่มนี้แล้ว</target> + <target>คุณเข้าร่วมกลุ่มนี้แล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve" approved="no"> + <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> <source>You joined this group. Connecting to inviting group member.</source> - <target state="translated">คุณเข้าร่วมกลุ่มนี้แล้ว กำลังเชื่อมต่อเพื่อเชิญสมาชิกกลุ่ม</target> + <target>คุณเข้าร่วมกลุ่มนี้แล้ว กำลังเชื่อมต่อเพื่อเชิญสมาชิกกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve" approved="no"> + <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> - <target state="translated">คุณต้องใช้ฐานข้อมูลแชทเวอร์ชันล่าสุดบนอุปกรณ์เครื่องเดียวเท่านั้น มิฉะนั้น คุณอาจหยุดได้รับข้อความจากผู้ติดต่อบางคน</target> + <target>คุณต้องใช้ฐานข้อมูลแชทเวอร์ชันล่าสุดบนอุปกรณ์เครื่องเดียวเท่านั้น มิฉะนั้น คุณอาจหยุดได้รับข้อความจากผู้ติดต่อบางคน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve" approved="no"> + <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> <source>You need to allow your contact to send voice messages to be able to send them.</source> - <target state="translated">คุณต้องอนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียงจึงจะสามารถส่งได้</target> + <target>คุณต้องอนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียงจึงจะสามารถส่งได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You rejected group invitation" xml:space="preserve" approved="no"> + <trans-unit id="You rejected group invitation" xml:space="preserve"> <source>You rejected group invitation</source> - <target state="translated">คุณปฏิเสธคำเชิญเข้าร่วมกลุ่ม</target> + <target>คุณปฏิเสธคำเชิญเข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You sent group invitation" xml:space="preserve" approved="no"> + <trans-unit id="You sent group invitation" xml:space="preserve"> <source>You sent group invitation</source> - <target state="translated">คุณส่งคำเชิญเข้าร่วมกลุ่มแล้ว</target> + <target>คุณส่งคำเชิญเข้าร่วมกลุ่มแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve" approved="no"> + <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected to group when the group host's device is online, please wait or check later!</source> - <target state="translated">คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target> + <target>คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve" approved="no"> + <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> <source>You will be connected when your connection request is accepted, please wait or check later!</source> - <target state="translated">คุณจะเชื่อมต่อเมื่อคำขอเชื่อมต่อของคุณได้รับการยอมรับ โปรดรอหรือตรวจสอบในภายหลัง!</target> + <target>คุณจะเชื่อมต่อเมื่อคำขอเชื่อมต่อของคุณได้รับการยอมรับ โปรดรอหรือตรวจสอบในภายหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve" approved="no"> + <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> <source>You will be connected when your contact's device is online, please wait or check later!</source> - <target state="translated">คุณจะเชื่อมต่อเมื่ออุปกรณ์ของผู้ติดต่อของคุณออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target> + <target>คุณจะเชื่อมต่อเมื่ออุปกรณ์ของผู้ติดต่อของคุณออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve" approved="no"> + <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> - <target state="translated">คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง</target> + <target>คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve" approved="no"> + <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> <source>You will join a group this link refers to and connect to its group members.</source> - <target state="translated">คุณจะเข้าร่วมกลุ่มที่ลิงก์นี้อ้างถึงและเชื่อมต่อกับสมาชิกในกลุ่ม</target> + <target>คุณจะเข้าร่วมกลุ่มที่ลิงก์นี้อ้างถึงและเชื่อมต่อกับสมาชิกในกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve" approved="no"> + <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> <source>You will still receive calls and notifications from muted profiles when they are active.</source> - <target state="translated">คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน</target> + <target>คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve" approved="no"> + <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> <source>You will stop receiving messages from this group. Chat history will be preserved.</source> - <target state="translated">คุณจะหยุดได้รับข้อความจากกลุ่มนี้ ประวัติการแชทจะถูกรักษาไว้</target> + <target>คุณจะหยุดได้รับข้อความจากกลุ่มนี้ ประวัติการแชทจะถูกรักษาไว้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve" approved="no"> + <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> <source>You won't lose your contacts if you later delete your address.</source> - <target state="translated">คุณจะไม่สูญเสียรายชื่อผู้ติดต่อของคุณหากคุณลบที่อยู่ในภายหลัง</target> + <target>คุณจะไม่สูญเสียรายชื่อผู้ติดต่อของคุณหากคุณลบที่อยู่ในภายหลัง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve" approved="no"> + <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> - <target state="translated">คุณกำลังพยายามเชิญผู้ติดต่อที่คุณแบ่งปันโปรไฟล์ที่ไม่ระบุตัวตนไปยังกลุ่มที่คุณใช้โปรไฟล์หลักของคุณ</target> + <target>คุณกำลังพยายามเชิญผู้ติดต่อที่คุณแบ่งปันโปรไฟล์ที่ไม่ระบุตัวตนไปยังกลุ่มที่คุณใช้โปรไฟล์หลักของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve" approved="no"> + <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> - <target state="translated">คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ</target> + <target>คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your %@ servers" xml:space="preserve" approved="no"> + <trans-unit id="Your %@ servers" xml:space="preserve"> <source>Your %@ servers</source> - <target state="translated">เซิร์ฟเวอร์ %@ ของคุณ</target> + <target>เซิร์ฟเวอร์ %@ ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your ICE servers" xml:space="preserve" approved="no"> + <trans-unit id="Your ICE servers" xml:space="preserve"> <source>Your ICE servers</source> - <target state="translated">เซิร์ฟเวอร์ ICE ของคุณ</target> + <target>เซิร์ฟเวอร์ ICE ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve" approved="no"> + <trans-unit id="Your SMP servers" xml:space="preserve"> <source>Your SMP servers</source> - <target state="translated">เซิร์ฟเวอร์ SMP ของคุณ</target> + <target>เซิร์ฟเวอร์ SMP ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your SimpleX address" xml:space="preserve" approved="no"> + <trans-unit id="Your SimpleX address" xml:space="preserve"> <source>Your SimpleX address</source> - <target state="translated">ที่อยู่ SimpleX ของคุณ</target> + <target>ที่อยู่ SimpleX ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your XFTP servers" xml:space="preserve" approved="no"> + <trans-unit id="Your XFTP servers" xml:space="preserve"> <source>Your XFTP servers</source> - <target state="translated">เซิร์ฟเวอร์ XFTP ของคุณ</target> + <target>เซิร์ฟเวอร์ XFTP ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your calls" xml:space="preserve" approved="no"> + <trans-unit id="Your calls" xml:space="preserve"> <source>Your calls</source> - <target state="translated">การโทรของคุณ</target> + <target>การโทรของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat database" xml:space="preserve" approved="no"> + <trans-unit id="Your chat database" xml:space="preserve"> <source>Your chat database</source> - <target state="translated">ฐานข้อมูลการแชทของคุณ</target> + <target>ฐานข้อมูลการแชทของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve" approved="no"> + <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> - <target state="translated">ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt</target> + <target>ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve" approved="no"> + <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> <source>Your chat profile will be sent to group members</source> - <target state="translated">โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</target> + <target>โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve" approved="no"> - <source>Your chat profile will be sent to your contact</source> - <target state="translated">โปรไฟล์การแชทของคุณจะถูกส่งไปยังผู้ติดต่อของคุณ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profiles" xml:space="preserve" approved="no"> + <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> - <target state="translated">โปรไฟล์แชทของคุณ</target> + <target>โปรไฟล์แชทของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chats" xml:space="preserve"> - <source>Your chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve" approved="no"> + <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> <source>Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link).</source> - <target state="translated">ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์ + <target>ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์ คุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve" approved="no"> + <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> - <target state="translated">ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)</target> + <target>ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve" approved="no"> + <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> <source>Your contacts can allow full message deletion.</source> - <target state="translated">ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้</target> + <target>ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve" approved="no"> + <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> <source>Your contacts in SimpleX will see it. You can change it in Settings.</source> - <target state="translated">ผู้ติดต่อของคุณใน SimpleX จะเห็น + <target>ผู้ติดต่อของคุณใน SimpleX จะเห็น คุณสามารถเปลี่ยนได้ในการตั้งค่า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your contacts will remain connected." xml:space="preserve" approved="no"> + <trans-unit id="Your contacts will remain connected." xml:space="preserve"> <source>Your contacts will remain connected.</source> - <target state="translated">ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</target> + <target>ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve" approved="no"> + <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> - <target state="translated">ฐานข้อมูลแชทปัจจุบันของคุณจะถูกลบและแทนที่ด้วยฐานข้อมูลที่นำเข้า</target> + <target>ฐานข้อมูลแชทปัจจุบันของคุณจะถูกลบและแทนที่ด้วยฐานข้อมูลที่นำเข้า</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your current profile" xml:space="preserve" approved="no"> + <trans-unit id="Your current profile" xml:space="preserve"> <source>Your current profile</source> - <target state="translated">โปรไฟล์ปัจจุบันของคุณ</target> + <target>โปรไฟล์ปัจจุบันของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your preferences" xml:space="preserve" approved="no"> + <trans-unit id="Your preferences" xml:space="preserve"> <source>Your preferences</source> - <target state="translated">การตั้งค่าของคุณ</target> + <target>การตั้งค่าของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your privacy" xml:space="preserve" approved="no"> + <trans-unit id="Your privacy" xml:space="preserve"> <source>Your privacy</source> - <target state="translated">ความเป็นส่วนตัวของคุณ</target> + <target>ความเป็นส่วนตัวของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve" approved="no"> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> - <target state="translated">โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น + <target>โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น เซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve" approved="no"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target state="translated">โปรไฟล์ของคุณจะถูกส่งไปยังผู้ติดต่อที่คุณได้รับลิงก์นี้จาก</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve" approved="no"> + <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> - <target state="translated">โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ</target> + <target>โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your random profile" xml:space="preserve" approved="no"> + <trans-unit id="Your random profile" xml:space="preserve"> <source>Your random profile</source> - <target state="translated">โปรไฟล์แบบสุ่มของคุณ</target> + <target>โปรไฟล์แบบสุ่มของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server" xml:space="preserve" approved="no"> + <trans-unit id="Your server" xml:space="preserve"> <source>Your server</source> - <target state="translated">เซิร์ฟเวอร์ของคุณ</target> + <target>เซิร์ฟเวอร์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your server address" xml:space="preserve" approved="no"> + <trans-unit id="Your server address" xml:space="preserve"> <source>Your server address</source> - <target state="translated">ที่อยู่เซิร์ฟเวอร์ของคุณ</target> + <target>ที่อยู่เซิร์ฟเวอร์ของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your settings" xml:space="preserve" approved="no"> + <trans-unit id="Your settings" xml:space="preserve"> <source>Your settings</source> - <target state="translated">การตั้งค่าของคุณ</target> + <target>การตั้งค่าของคุณ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve" approved="no"> + <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> - <target state="translated">[มีส่วนร่วม](https://github.com/simplex-chat/simplex-chat#contribute)</target> + <target>[มีส่วนร่วม](https://github.com/simplex-chat/simplex-chat#contribute)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve" approved="no"> + <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> <source>[Send us email](mailto:chat@simplex.chat)</source> - <target state="translated">[ส่งอีเมลถึงเรา](mailto:chat@simplex.chat)</target> + <target>[ส่งอีเมลถึงเรา](mailto:chat@simplex.chat)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve" approved="no"> + <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> - <target state="translated">[ติดดาวบน GitHub](https://github.com/simplex-chat/simplex-chat)</target> + <target>[ติดดาวบน GitHub](https://github.com/simplex-chat/simplex-chat)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="_italic_" xml:space="preserve" approved="no"> + <trans-unit id="_italic_" xml:space="preserve"> <source>\_italic_</source> - <target state="translated">\_ตัวเอียง_</target> + <target>\_ตัวเอียง_</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="`a + b`" xml:space="preserve" approved="no"> + <trans-unit id="`a + b`" xml:space="preserve"> <source>\`a + b`</source> - <target state="translated">\`a + b`</target> + <target>\`a + b`</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="above, then choose:" xml:space="preserve" approved="no"> + <trans-unit id="above, then choose:" xml:space="preserve"> <source>above, then choose:</source> - <target state="translated">ด้านบน จากนั้นเลือก:</target> + <target>ด้านบน จากนั้นเลือก:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="accepted call" xml:space="preserve" approved="no"> + <trans-unit id="accepted call" xml:space="preserve"> <source>accepted call</source> - <target state="translated">รับสายแล้ว</target> + <target>รับสายแล้ว</target> <note>call status</note> </trans-unit> - <trans-unit id="admin" xml:space="preserve" approved="no"> + <trans-unit id="admin" xml:space="preserve"> <source>admin</source> - <target state="translated">ผู้ดูแลระบบ</target> + <target>ผู้ดูแลระบบ</target> <note>member role</note> </trans-unit> - <trans-unit id="always" xml:space="preserve" approved="no"> + <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> + <source>agreeing encryption for %@…</source> + <target>ยอมรับ encryption สำหรับ %@…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="agreeing encryption…" xml:space="preserve"> + <source>agreeing encryption…</source> + <target>เห็นด้วยกับการ encryption…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="always" xml:space="preserve"> <source>always</source> - <target state="translated">เสมอ</target> + <target>เสมอ</target> <note>pref value</note> </trans-unit> - <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve" approved="no"> + <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> <source>audio call (not e2e encrypted)</source> - <target state="translated">การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</target> + <target>การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="bad message ID" xml:space="preserve" approved="no"> + <trans-unit id="bad message ID" xml:space="preserve"> <source>bad message ID</source> - <target state="translated">ID ข้อความที่ไม่ดี</target> + <target>ID ข้อความที่ไม่ดี</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="bad message hash" xml:space="preserve" approved="no"> + <trans-unit id="bad message hash" xml:space="preserve"> <source>bad message hash</source> - <target state="translated">แฮชข้อความไม่ดี</target> + <target>แฮชข้อความไม่ดี</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="bold" xml:space="preserve" approved="no"> + <trans-unit id="bold" xml:space="preserve"> <source>bold</source> - <target state="translated">ตัวหนา</target> + <target>ตัวหนา</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="call error" xml:space="preserve" approved="no"> + <trans-unit id="call error" xml:space="preserve"> <source>call error</source> - <target state="translated">การโทรผิดพลาด</target> + <target>การโทรผิดพลาด</target> <note>call status</note> </trans-unit> - <trans-unit id="call in progress" xml:space="preserve" approved="no"> + <trans-unit id="call in progress" xml:space="preserve"> <source>call in progress</source> - <target state="translated">กําลังโทร</target> + <target>กําลังโทร</target> <note>call status</note> </trans-unit> - <trans-unit id="calling…" xml:space="preserve" approved="no"> + <trans-unit id="calling…" xml:space="preserve"> <source>calling…</source> - <target state="translated">กำลังโทร…</target> + <target>กำลังโทร…</target> <note>call status</note> </trans-unit> - <trans-unit id="cancelled %@" xml:space="preserve" approved="no"> + <trans-unit id="cancelled %@" xml:space="preserve"> <source>cancelled %@</source> - <target state="translated">ยกเลิก %@</target> + <target>ยกเลิก %@</target> <note>feature offered item</note> </trans-unit> - <trans-unit id="changed address for you" xml:space="preserve" approved="no"> + <trans-unit id="changed address for you" xml:space="preserve"> <source>changed address for you</source> - <target state="translated">เปลี่ยนที่อยู่สําหรับคุณ</target> + <target>เปลี่ยนที่อยู่สําหรับคุณแล้ว</target> <note>chat item text</note> </trans-unit> - <trans-unit id="changed role of %@ to %@" xml:space="preserve" approved="no"> + <trans-unit id="changed role of %@ to %@" xml:space="preserve"> <source>changed role of %1$@ to %2$@</source> - <target state="translated">เปลี่ยนบทบาทของ %1$@ เป็น %2$@</target> + <target>เปลี่ยนบทบาทของ %1$@ เป็น %2$@ แล้ว</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="changed your role to %@" xml:space="preserve" approved="no"> + <trans-unit id="changed your role to %@" xml:space="preserve"> <source>changed your role to %@</source> - <target state="translated">เปลี่ยนบทบาทของคุณเป็น %@</target> + <target>เปลี่ยนบทบาทของคุณเป็น %@</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="changing address for %@..." xml:space="preserve" approved="no"> - <source>changing address for %@...</source> - <target state="translated">เปลี่ยนที่อยู่สำหรับ %@...</target> + <trans-unit id="changing address for %@…" xml:space="preserve"> + <source>changing address for %@…</source> + <target>เปลี่ยนที่อยู่สำหรับ %@…</target> <note>chat item text</note> </trans-unit> - <trans-unit id="changing address..." xml:space="preserve" approved="no"> - <source>changing address...</source> - <target state="translated">เปลี่ยนที่อยู่...</target> + <trans-unit id="changing address…" xml:space="preserve"> + <source>changing address…</source> + <target>กำลังเปลี่ยนที่อยู่…</target> <note>chat item text</note> </trans-unit> - <trans-unit id="colored" xml:space="preserve" approved="no"> + <trans-unit id="colored" xml:space="preserve"> <source>colored</source> - <target state="translated">มีสี</target> + <target>มีสี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="complete" xml:space="preserve" approved="no"> + <trans-unit id="complete" xml:space="preserve"> <source>complete</source> - <target state="translated">สมบูรณ์</target> + <target>สมบูรณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve" approved="no"> + <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> <source>connect to SimpleX Chat developers.</source> - <target state="translated">เชื่อมต่อกับนักพัฒนา SimpleX Chat</target> + <target>เชื่อมต่อกับนักพัฒนา SimpleX Chat</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connected" xml:space="preserve" approved="no"> + <trans-unit id="connected" xml:space="preserve"> <source>connected</source> - <target state="translated">เชื่อมต่อสำเร็จ</target> + <target>เชื่อมต่อสำเร็จ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connecting" xml:space="preserve" approved="no"> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> - <target state="translated">กำลังเชื่อมต่อ</target> + <target>กำลังเชื่อมต่อ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connecting (accepted)" xml:space="preserve" approved="no"> + <trans-unit id="connecting (accepted)" xml:space="preserve"> <source>connecting (accepted)</source> - <target state="translated">กำลังเชื่อมต่อ (ยอมรับแล้ว)</target> + <target>กำลังเชื่อมต่อ (ยอมรับแล้ว)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connecting (announced)" xml:space="preserve" approved="no"> + <trans-unit id="connecting (announced)" xml:space="preserve"> <source>connecting (announced)</source> - <target state="translated">กำลังเชื่อมต่อ (ประกาศแล้ว)</target> + <target>กำลังเชื่อมต่อ (ประกาศแล้ว)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connecting (introduced)" xml:space="preserve" approved="no"> + <trans-unit id="connecting (introduced)" xml:space="preserve"> <source>connecting (introduced)</source> - <target state="translated">กำลังการเชื่อมต่อ (แนะนําแล้ว)</target> + <target>กำลังเชื่อมต่อ (แนะนํา)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connecting (introduction invitation)" xml:space="preserve" approved="no"> + <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> <source>connecting (introduction invitation)</source> - <target state="translated">กำลังเชื่อมต่อ (คําเชิญแนะนํา)</target> + <target>กำลังเชื่อมต่อ (คําเชิญแนะนํา)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="connecting call" xml:space="preserve" approved="no"> + <trans-unit id="connecting call" xml:space="preserve"> <source>connecting call…</source> - <target state="translated">กําลังเชื่อมต่อสาย…</target> + <target>กําลังเชื่อมต่อสาย…</target> <note>call status</note> </trans-unit> - <trans-unit id="connecting…" xml:space="preserve" approved="no"> + <trans-unit id="connecting…" xml:space="preserve"> <source>connecting…</source> - <target state="translated">กำลังเชื่อมต่อ…</target> + <target>กำลังเชื่อมต่อ…</target> <note>chat list item title</note> </trans-unit> - <trans-unit id="connection established" xml:space="preserve" approved="no"> + <trans-unit id="connection established" xml:space="preserve"> <source>connection established</source> - <target state="translated">สร้างการเชื่อมต่อแล้ว</target> + <target>สร้างการเชื่อมต่อแล้ว</target> <note>chat list item title (it should not be shown</note> </trans-unit> - <trans-unit id="connection:%@" xml:space="preserve" approved="no"> + <trans-unit id="connection:%@" xml:space="preserve"> <source>connection:%@</source> - <target state="translated">การเชื่อมต่อ:%@</target> + <target>การเชื่อมต่อ:%@</target> <note>connection information</note> </trans-unit> - <trans-unit id="contact has e2e encryption" xml:space="preserve" approved="no"> + <trans-unit id="contact has e2e encryption" xml:space="preserve"> <source>contact has e2e encryption</source> - <target state="translated">ผู้ติดต่อมีการ encrypt จากต้นจนจบ</target> + <target>ผู้ติดต่อมีการ encrypt จากต้นจนจบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="contact has no e2e encryption" xml:space="preserve" approved="no"> + <trans-unit id="contact has no e2e encryption" xml:space="preserve"> <source>contact has no e2e encryption</source> - <target state="translated">ผู้ติดต่อไม่มีการ encrypt จากต้นจนจบ</target> + <target>ผู้ติดต่อไม่มีการ encrypt จากต้นจนจบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="creator" xml:space="preserve" approved="no"> + <trans-unit id="creator" xml:space="preserve"> <source>creator</source> - <target state="translated">ผู้สร้าง</target> + <target>ผู้สร้าง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="custom" xml:space="preserve" approved="no"> + <trans-unit id="custom" xml:space="preserve"> <source>custom</source> - <target state="translated">กำหนดเอง</target> + <target>กำหนดเอง</target> <note>dropdown time picker choice</note> </trans-unit> - <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve" approved="no"> + <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> <source>database version is newer than the app, but no down migration for: %@</source> - <target state="translated">เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลสำหรับ: %@</target> + <target>เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลลงสำหรับ: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="days" xml:space="preserve" approved="no"> + <trans-unit id="days" xml:space="preserve"> <source>days</source> - <target state="translated">วัน</target> + <target>วัน</target> <note>time unit</note> </trans-unit> - <trans-unit id="default (%@)" xml:space="preserve" approved="no"> + <trans-unit id="default (%@)" xml:space="preserve"> <source>default (%@)</source> - <target state="translated">ค่าเริ่มต้น (%@)</target> + <target>ค่าเริ่มต้น (%@)</target> <note>pref value</note> </trans-unit> - <trans-unit id="deleted" xml:space="preserve" approved="no"> + <trans-unit id="default (no)" xml:space="preserve"> + <source>default (no)</source> + <target>ค่าเริ่มต้น (ไม่)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="default (yes)" xml:space="preserve"> + <source>default (yes)</source> + <target>ค่าเริ่มต้น (ใช่)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="deleted" xml:space="preserve"> <source>deleted</source> - <target state="translated">ลบแล้ว</target> + <target>ลบแล้ว</target> <note>deleted chat item</note> </trans-unit> - <trans-unit id="deleted group" xml:space="preserve" approved="no"> + <trans-unit id="deleted group" xml:space="preserve"> <source>deleted group</source> - <target state="translated">กลุ่มที่ถูกลบ</target> + <target>กลุ่มที่ถูกลบ</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve" approved="no"> + <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> <source>different migration in the app/database: %@ / %@</source> - <target state="translated">การโยกย้ายที่แตกต่างกันในแอป/ฐานข้อมูล: %@ / %@</target> + <target>การโยกย้ายที่แตกต่างกันในแอป/ฐานข้อมูล: %@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="direct" xml:space="preserve" approved="no"> + <trans-unit id="direct" xml:space="preserve"> <source>direct</source> - <target state="translated">โดยตรง</target> + <target>โดยตรง</target> <note>connection level description</note> </trans-unit> - <trans-unit id="duplicate message" xml:space="preserve" approved="no"> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> - <target state="translated">ข้อความที่ซ้ำกัน</target> + <target>ข้อความที่ซ้ำกัน</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="e2e encrypted" xml:space="preserve" approved="no"> + <trans-unit id="e2e encrypted" xml:space="preserve"> <source>e2e encrypted</source> - <target state="translated">encrypted จากต้นจนจบ</target> + <target>encrypted จากต้นจนจบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="enabled" xml:space="preserve" approved="no"> + <trans-unit id="enabled" xml:space="preserve"> <source>enabled</source> - <target state="translated">เปิดใช้งาน</target> + <target>เปิดใช้งาน</target> <note>enabled status</note> </trans-unit> - <trans-unit id="enabled for contact" xml:space="preserve" approved="no"> + <trans-unit id="enabled for contact" xml:space="preserve"> <source>enabled for contact</source> - <target state="translated">ได้เปิดใช้งานสำหรับการติดต่อแล้ว</target> + <target>ได้เปิดใช้งานสำหรับการติดต่อแล้ว</target> <note>enabled status</note> </trans-unit> - <trans-unit id="enabled for you" xml:space="preserve" approved="no"> + <trans-unit id="enabled for you" xml:space="preserve"> <source>enabled for you</source> - <target state="translated">เปิดใช้งานสําหรับคุณแล้ว</target> + <target>เปิดใช้งานสําหรับคุณแล้ว</target> <note>enabled status</note> </trans-unit> - <trans-unit id="ended" xml:space="preserve" approved="no"> + <trans-unit id="encryption agreed" xml:space="preserve"> + <source>encryption agreed</source> + <target>ตกลง encryption</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption agreed for %@" xml:space="preserve"> + <source>encryption agreed for %@</source> + <target>ตกลง encryption สําหรับ % @</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok" xml:space="preserve"> + <source>encryption ok</source> + <target>encryptionใช้ได้</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok for %@" xml:space="preserve"> + <source>encryption ok for %@</source> + <target>encryptionใช้ได้สําหรับ %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> + <source>encryption re-negotiation allowed</source> + <target>อนุญาตให้มีการเจรจา encryption อีกครั้ง</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> + <source>encryption re-negotiation allowed for %@</source> + <target>อนุญาตให้มีการเจรจา encryption อีกครั้งสําหรับ %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required" xml:space="preserve"> + <source>encryption re-negotiation required</source> + <target>จำเป็นต้องมีการเจรจา encryption อีกครั้ง</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> + <source>encryption re-negotiation required for %@</source> + <target>จำเป็นต้องมีการเจรจา encryption อีกครั้งสําหรับ %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="ended" xml:space="preserve"> <source>ended</source> - <target state="translated">สิ้นสุดลงแล้ว</target> + <target>สิ้นสุดลงแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="ended call %@" xml:space="preserve" approved="no"> + <trans-unit id="ended call %@" xml:space="preserve"> <source>ended call %@</source> - <target state="translated">สิ้นสุดการโทร %@</target> + <target>สิ้นสุดการโทร %@</target> <note>call status</note> </trans-unit> - <trans-unit id="error" xml:space="preserve" approved="no"> + <trans-unit id="error" xml:space="preserve"> <source>error</source> - <target state="translated">ผิดพลาด</target> + <target>ผิดพลาด</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="group deleted" xml:space="preserve" approved="no"> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> - <target state="translated">ลบกลุ่มแล้ว</target> + <target>ลบกลุ่มแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="group profile updated" xml:space="preserve" approved="no"> + <trans-unit id="group profile updated" xml:space="preserve"> <source>group profile updated</source> - <target state="translated">อัปเดตโปรไฟล์กลุ่มแล้ว</target> + <target>อัปเดตโปรไฟล์กลุ่มแล้ว</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="hours" xml:space="preserve" approved="no"> + <trans-unit id="hours" xml:space="preserve"> <source>hours</source> - <target state="translated">ชั่วโมง</target> + <target>ชั่วโมง</target> <note>time unit</note> </trans-unit> - <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve" approved="no"> + <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> - <target state="translated">iOS Keychain ใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัย - อนุญาตให้รับการแจ้งเตือนแบบทันที</target> + <target>iOS Keychain ใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัย - อนุญาตให้รับการแจ้งเตือนแบบทันที</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve" approved="no"> + <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> - <target state="translated">iOS Keychain จะใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัยหลังจากที่คุณรีสตาร์ทแอปหรือเปลี่ยนรหัสผ่าน ซึ่งจะช่วยให้รับการแจ้งเตือนแบบทันทีได้</target> + <target>iOS Keychain จะใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัยหลังจากที่คุณรีสตาร์ทแอปหรือเปลี่ยนรหัสผ่าน ซึ่งจะช่วยให้รับการแจ้งเตือนแบบทันทีได้</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="incognito via contact address link" xml:space="preserve" approved="no"> + <trans-unit id="incognito via contact address link" xml:space="preserve"> <source>incognito via contact address link</source> - <target state="translated">ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ</target> + <target>ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="incognito via group link" xml:space="preserve" approved="no"> + <trans-unit id="incognito via group link" xml:space="preserve"> <source>incognito via group link</source> - <target state="translated">ไม่ระบุตัวตนผ่านลิงก์กลุ่ม</target> + <target>ไม่ระบุตัวตนผ่านลิงก์กลุ่ม</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="incognito via one-time link" xml:space="preserve" approved="no"> + <trans-unit id="incognito via one-time link" xml:space="preserve"> <source>incognito via one-time link</source> - <target state="translated">ไม่ระบุตัวตนผ่านลิงก์แบบครั้งเดียว</target> + <target>ไม่ระบุตัวตนผ่านลิงก์แบบใช้ครั้งเดียว</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="indirect (%d)" xml:space="preserve" approved="no"> + <trans-unit id="indirect (%d)" xml:space="preserve"> <source>indirect (%d)</source> - <target state="translated">ทางอ้อม (%d)</target> + <target>ทางอ้อม (%d)</target> <note>connection level description</note> </trans-unit> - <trans-unit id="invalid chat" xml:space="preserve" approved="no"> + <trans-unit id="invalid chat" xml:space="preserve"> <source>invalid chat</source> - <target state="translated">แชทไม่ถูกต้อง</target> + <target>แชทไม่ถูกต้อง</target> <note>invalid chat data</note> </trans-unit> - <trans-unit id="invalid chat data" xml:space="preserve" approved="no"> + <trans-unit id="invalid chat data" xml:space="preserve"> <source>invalid chat data</source> - <target state="translated">ข้อมูลแชทไม่ถูกต้อง</target> + <target>ข้อมูลแชทไม่ถูกต้อง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="invalid data" xml:space="preserve" approved="no"> + <trans-unit id="invalid data" xml:space="preserve"> <source>invalid data</source> - <target state="translated">ข้อมูลไม่ถูกต้อง</target> + <target>ข้อมูลไม่ถูกต้อง</target> <note>invalid chat item</note> </trans-unit> - <trans-unit id="invitation to group %@" xml:space="preserve" approved="no"> + <trans-unit id="invitation to group %@" xml:space="preserve"> <source>invitation to group %@</source> - <target state="translated">คำเชิญเข้าร่วมกลุ่ม %@</target> + <target>คำเชิญเข้าร่วมกลุ่ม %@</target> <note>group name</note> </trans-unit> - <trans-unit id="invited" xml:space="preserve" approved="no"> + <trans-unit id="invited" xml:space="preserve"> <source>invited</source> - <target state="translated">เชิญ</target> + <target>เชิญ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="invited %@" xml:space="preserve" approved="no"> + <trans-unit id="invited %@" xml:space="preserve"> <source>invited %@</source> - <target state="translated">เชิญ %@</target> + <target>เชิญ %@</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="invited to connect" xml:space="preserve" approved="no"> + <trans-unit id="invited to connect" xml:space="preserve"> <source>invited to connect</source> - <target state="translated">ได้รับเชิญให้เชื่อมต่อ</target> + <target>ได้รับเชิญให้เชื่อมต่อ</target> <note>chat list item title</note> </trans-unit> - <trans-unit id="invited via your group link" xml:space="preserve" approved="no"> + <trans-unit id="invited via your group link" xml:space="preserve"> <source>invited via your group link</source> - <target state="translated">เชิญผ่านลิงค์กลุ่มของคุณ</target> + <target>ถูกเชิญผ่านลิงค์กลุ่มของคุณ</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="italic" xml:space="preserve" approved="no"> + <trans-unit id="italic" xml:space="preserve"> <source>italic</source> - <target state="translated">ตัวเอียง</target> + <target>ตัวเอียง</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="join as %@" xml:space="preserve" approved="no"> + <trans-unit id="join as %@" xml:space="preserve"> <source>join as %@</source> - <target state="translated">เข้าร่วมเป็น %@</target> + <target>เข้าร่วมเป็น %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="left" xml:space="preserve" approved="no"> + <trans-unit id="left" xml:space="preserve"> <source>left</source> - <target state="translated">ออกแล้ว</target> + <target>ออกแล้ว</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="marked deleted" xml:space="preserve" approved="no"> + <trans-unit id="marked deleted" xml:space="preserve"> <source>marked deleted</source> - <target state="translated">ทำเครื่องหมายว่าลบแล้ว</target> + <target>ทำเครื่องหมายว่าลบแล้ว</target> <note>marked deleted chat item preview text</note> </trans-unit> - <trans-unit id="member" xml:space="preserve" approved="no"> + <trans-unit id="member" xml:space="preserve"> <source>member</source> - <target state="translated">สมาชิก</target> + <target>สมาชิก</target> <note>member role</note> </trans-unit> - <trans-unit id="member connected" xml:space="preserve" approved="no"> + <trans-unit id="member connected" xml:space="preserve"> <source>connected</source> - <target state="translated">เชื่อมต่อสำเร็จ</target> + <target>เชื่อมต่อสำเร็จ</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="message received" xml:space="preserve" approved="no"> + <trans-unit id="message received" xml:space="preserve"> <source>message received</source> - <target state="translated">ข้อความที่ได้รับ</target> + <target>ข้อความที่ได้รับ</target> <note>notification</note> </trans-unit> - <trans-unit id="minutes" xml:space="preserve" approved="no"> + <trans-unit id="minutes" xml:space="preserve"> <source>minutes</source> - <target state="translated">นาที</target> + <target>นาที</target> <note>time unit</note> </trans-unit> - <trans-unit id="missed call" xml:space="preserve" approved="no"> + <trans-unit id="missed call" xml:space="preserve"> <source>missed call</source> - <target state="translated">สายที่ไม่ได้รับ</target> + <target>สายที่ไม่ได้รับ</target> <note>call status</note> </trans-unit> - <trans-unit id="moderated" xml:space="preserve" approved="no"> + <trans-unit id="moderated" xml:space="preserve"> <source>moderated</source> - <target state="translated">กลั่นกรองแล้ว</target> + <target>กลั่นกรองแล้ว</target> <note>moderated chat item</note> </trans-unit> - <trans-unit id="moderated by %@" xml:space="preserve" approved="no"> + <trans-unit id="moderated by %@" xml:space="preserve"> <source>moderated by %@</source> - <target state="translated">กลั่นกรองโดย %@</target> + <target>กลั่นกรองโดย %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="months" xml:space="preserve" approved="no"> + <trans-unit id="months" xml:space="preserve"> <source>months</source> - <target state="translated">เดือน</target> + <target>เดือน</target> <note>time unit</note> </trans-unit> - <trans-unit id="never" xml:space="preserve" approved="no"> + <trans-unit id="never" xml:space="preserve"> <source>never</source> - <target state="translated">ไม่เคย</target> + <target>ไม่เคย</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="new message" xml:space="preserve" approved="no"> + <trans-unit id="new message" xml:space="preserve"> <source>new message</source> - <target state="translated">ข้อความใหม่</target> + <target>ข้อความใหม่</target> <note>notification</note> </trans-unit> - <trans-unit id="no" xml:space="preserve" approved="no"> + <trans-unit id="no" xml:space="preserve"> <source>no</source> - <target state="translated">เลขที่</target> + <target>ไม่</target> <note>pref value</note> </trans-unit> - <trans-unit id="no e2e encryption" xml:space="preserve" approved="no"> + <trans-unit id="no e2e encryption" xml:space="preserve"> <source>no e2e encryption</source> - <target state="translated">ไม่มีการ encrypt จากต้นจนจบ</target> + <target>ไม่มีการ encrypt จากต้นจนจบ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="no text" xml:space="preserve" approved="no"> + <trans-unit id="no text" xml:space="preserve"> <source>no text</source> - <target state="translated">ไม่มีข้อความ</target> + <target>ไม่มีข้อความ</target> <note>copied message info in history</note> </trans-unit> - <trans-unit id="observer" xml:space="preserve" approved="no"> + <trans-unit id="observer" xml:space="preserve"> <source>observer</source> - <target state="translated">ผู้สังเกตการณ์</target> + <target>ผู้สังเกตการณ์</target> <note>member role</note> </trans-unit> - <trans-unit id="off" xml:space="preserve" approved="no"> + <trans-unit id="off" xml:space="preserve"> <source>off</source> - <target state="translated">ปิด</target> + <target>ปิด</target> <note>enabled status group pref value</note> </trans-unit> - <trans-unit id="offered %@" xml:space="preserve" approved="no"> + <trans-unit id="offered %@" xml:space="preserve"> <source>offered %@</source> - <target state="translated">เสนอ %@</target> + <target>เสนอแล้ว %@</target> <note>feature offered item</note> </trans-unit> - <trans-unit id="offered %@: %@" xml:space="preserve" approved="no"> + <trans-unit id="offered %@: %@" xml:space="preserve"> <source>offered %1$@: %2$@</source> - <target state="translated">เสนอ %1$@: %2$@</target> + <target>เสนอแล้ว %1$@: %2$@</target> <note>feature offered item</note> </trans-unit> - <trans-unit id="on" xml:space="preserve" approved="no"> + <trans-unit id="on" xml:space="preserve"> <source>on</source> - <target state="translated">เปิด</target> + <target>เปิด</target> <note>group pref value</note> </trans-unit> - <trans-unit id="or chat with the developers" xml:space="preserve" approved="no"> + <trans-unit id="or chat with the developers" xml:space="preserve"> <source>or chat with the developers</source> - <target state="translated">หรือแชทกับนักพัฒนาแอป</target> + <target>หรือแชทกับนักพัฒนาแอป</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="owner" xml:space="preserve" approved="no"> + <trans-unit id="owner" xml:space="preserve"> <source>owner</source> - <target state="translated">เจ้าของ</target> + <target>เจ้าของ</target> <note>member role</note> </trans-unit> - <trans-unit id="peer-to-peer" xml:space="preserve" approved="no"> + <trans-unit id="peer-to-peer" xml:space="preserve"> <source>peer-to-peer</source> - <target state="translated">เพื่อนต่อเพื่อน</target> + <target>เพื่อนต่อเพื่อน</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="received answer…" xml:space="preserve" approved="no"> + <trans-unit id="received answer…" xml:space="preserve"> <source>received answer…</source> - <target state="translated">ได้รับคำตอบ…</target> + <target>ได้รับคำตอบ…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="received confirmation…" xml:space="preserve" approved="no"> + <trans-unit id="received confirmation…" xml:space="preserve"> <source>received confirmation…</source> - <target state="translated">ได้รับการยืนยัน…</target> + <target>ได้รับการยืนยัน…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="rejected call" xml:space="preserve" approved="no"> + <trans-unit id="rejected call" xml:space="preserve"> <source>rejected call</source> - <target state="translated">สายถูกปฏิเสธ</target> + <target>สายถูกปฏิเสธ</target> <note>call status</note> </trans-unit> - <trans-unit id="removed" xml:space="preserve" approved="no"> + <trans-unit id="removed" xml:space="preserve"> <source>removed</source> - <target state="translated">ถูกลบแล้ว</target> + <target>ถูกลบแล้ว</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="removed %@" xml:space="preserve" approved="no"> + <trans-unit id="removed %@" xml:space="preserve"> <source>removed %@</source> - <target state="translated">ถูกลบแล้ว %@</target> + <target>ถูกลบแล้ว %@</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="removed you" xml:space="preserve" approved="no"> + <trans-unit id="removed you" xml:space="preserve"> <source>removed you</source> - <target state="translated">ลบคุณออกแล้ว</target> + <target>ลบคุณออกแล้ว</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="sec" xml:space="preserve" approved="no"> + <trans-unit id="sec" xml:space="preserve"> <source>sec</source> - <target state="translated">วินาที</target> + <target>วินาที</target> <note>network option</note> </trans-unit> - <trans-unit id="seconds" xml:space="preserve" approved="no"> + <trans-unit id="seconds" xml:space="preserve"> <source>seconds</source> - <target state="translated">วินาที</target> + <target>วินาที</target> <note>time unit</note> </trans-unit> - <trans-unit id="secret" xml:space="preserve" approved="no"> + <trans-unit id="secret" xml:space="preserve"> <source>secret</source> - <target state="translated">ความลับ</target> + <target>ความลับ</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="starting…" xml:space="preserve" approved="no"> + <trans-unit id="security code changed" xml:space="preserve"> + <source>security code changed</source> + <target>เปลี่ยนรหัสความปลอดภัยแล้ว</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> - <target state="translated">เริ่มต้น…</target> + <target>กำลังเริ่มต้น…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="strike" xml:space="preserve" approved="no"> + <trans-unit id="strike" xml:space="preserve"> <source>strike</source> - <target state="translated">ตี</target> + <target>ตี</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="this contact" xml:space="preserve" approved="no"> + <trans-unit id="this contact" xml:space="preserve"> <source>this contact</source> - <target state="translated">ผู้ติดต่อนี้</target> + <target>ผู้ติดต่อนี้</target> <note>notification title</note> </trans-unit> - <trans-unit id="unknown" xml:space="preserve" approved="no"> + <trans-unit id="unknown" xml:space="preserve"> <source>unknown</source> - <target state="translated">ไม่ทราบ</target> + <target>ไม่ทราบ</target> <note>connection info</note> </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve" approved="no"> + <trans-unit id="updated group profile" xml:space="preserve"> <source>updated group profile</source> - <target state="translated">อัปเดตโปรไฟล์กลุ่มแล้ว</target> + <target>อัปเดตโปรไฟล์กลุ่มแล้ว</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="v%@ (%@)" xml:space="preserve" approved="no"> + <trans-unit id="v%@ (%@)" xml:space="preserve"> <source>v%@ (%@)</source> - <target state="translated">v%@ (%@)</target> + <target>v%@ (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve" approved="no"> + <trans-unit id="via contact address link" xml:space="preserve"> <source>via contact address link</source> - <target state="translated">ผ่านลิงค์ที่อยู่ติดต่อ</target> + <target>ผ่านลิงค์ที่อยู่ติดต่อ</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via group link" xml:space="preserve" approved="no"> + <trans-unit id="via group link" xml:space="preserve"> <source>via group link</source> - <target state="translated">ผ่านลิงค์กลุ่ม</target> + <target>ผ่านลิงค์กลุ่ม</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve" approved="no"> + <trans-unit id="via one-time link" xml:space="preserve"> <source>via one-time link</source> - <target state="translated">ผ่านลิงค์แบบครั้งเดียว</target> + <target>ผ่านลิงค์แบบใช้ครั้งเดียว</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via relay" xml:space="preserve" approved="no"> + <trans-unit id="via relay" xml:space="preserve"> <source>via relay</source> - <target state="translated">ผ่านรีเลย์</target> + <target>ผ่านรีเลย์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve" approved="no"> + <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> <source>video call (not e2e encrypted)</source> - <target state="translated">การสนทนาทางวิดีโอ (ไม่ได้ encrypt จากต้นจนจบ)</target> + <target>การสนทนาทางวิดีโอ (ไม่ได้ encrypt จากต้นจนจบ)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve" approved="no"> + <trans-unit id="waiting for answer…" xml:space="preserve"> <source>waiting for answer…</source> - <target state="translated">รอคำตอบ…</target> + <target>รอคำตอบ…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve" approved="no"> + <trans-unit id="waiting for confirmation…" xml:space="preserve"> <source>waiting for confirmation…</source> - <target state="translated">รอการยืนยัน…</target> + <target>รอการยืนยัน…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve" approved="no"> + <trans-unit id="wants to connect to you!" xml:space="preserve"> <source>wants to connect to you!</source> - <target state="translated">ต้องการเชื่อมต่อกับคุณ!</target> + <target>ต้องการเชื่อมต่อกับคุณ!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="weeks" xml:space="preserve" approved="no"> + <trans-unit id="weeks" xml:space="preserve"> <source>weeks</source> - <target state="translated">สัปดาห์</target> + <target>สัปดาห์</target> <note>time unit</note> </trans-unit> - <trans-unit id="yes" xml:space="preserve" approved="no"> + <trans-unit id="yes" xml:space="preserve"> <source>yes</source> - <target state="translated">ใช่</target> + <target>ใช่</target> <note>pref value</note> </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve" approved="no"> + <trans-unit id="you are invited to group" xml:space="preserve"> <source>you are invited to group</source> - <target state="translated">คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target> + <target>คุณได้รับเชิญให้เข้าร่วมกลุ่ม</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you are observer" xml:space="preserve" approved="no"> + <trans-unit id="you are observer" xml:space="preserve"> <source>you are observer</source> - <target state="translated">คุณเป็นผู้สังเกตการณ์</target> + <target>คุณเป็นผู้สังเกตการณ์</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you changed address" xml:space="preserve" approved="no"> + <trans-unit id="you changed address" xml:space="preserve"> <source>you changed address</source> - <target state="translated">คุณเปลี่ยนที่อยู่</target> + <target>คุณเปลี่ยนที่อยู่แล้ว</target> <note>chat item text</note> </trans-unit> - <trans-unit id="you changed address for %@" xml:space="preserve" approved="no"> + <trans-unit id="you changed address for %@" xml:space="preserve"> <source>you changed address for %@</source> - <target state="translated">คุณเปลี่ยนที่อยู่สำหรับ %@</target> + <target>คุณเปลี่ยนที่อยู่สำหรับ %@</target> <note>chat item text</note> </trans-unit> - <trans-unit id="you changed role for yourself to %@" xml:space="preserve" approved="no"> + <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> <source>you changed role for yourself to %@</source> - <target state="translated">คุณเปลี่ยนบทบาทสำหรับตัวคุณเองเป็น %@</target> + <target>คุณเปลี่ยนบทบาทสำหรับตัวคุณเองเป็น %@</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you changed role of %@ to %@" xml:space="preserve" approved="no"> + <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> <source>you changed role of %1$@ to %2$@</source> - <target state="translated">คุณเปลี่ยนบทบาทของ %1$@ เป็น %2$@</target> + <target>คุณเปลี่ยนบทบาทของ %1$@ เป็น %2$@</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you left" xml:space="preserve" approved="no"> + <trans-unit id="you left" xml:space="preserve"> <source>you left</source> - <target state="translated">คุณออกไปแล้ว</target> + <target>คุณออกไปแล้ว</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you removed %@" xml:space="preserve" approved="no"> + <trans-unit id="you removed %@" xml:space="preserve"> <source>you removed %@</source> - <target state="translated">คุณลบ %@</target> + <target>คุณลบ %@</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve" approved="no"> + <trans-unit id="you shared one-time link" xml:space="preserve"> <source>you shared one-time link</source> - <target state="translated">คุณแชร์ลิงก์แบบใช้ครั้งเดียว</target> + <target>คุณแชร์ลิงก์แบบใช้ครั้งเดียวแล้ว</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="you shared one-time link incognito" xml:space="preserve" approved="no"> + <trans-unit id="you shared one-time link incognito" xml:space="preserve"> <source>you shared one-time link incognito</source> - <target state="translated">คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตน</target> + <target>คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตนแล้ว</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="you: " xml:space="preserve" approved="no"> + <trans-unit id="you: " xml:space="preserve"> <source>you: </source> - <target state="translated">คุณ: </target> + <target>คุณ: </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="~strike~" xml:space="preserve" approved="no"> + <trans-unit id="~strike~" xml:space="preserve"> <source>\~strike~</source> - <target state="translated">\~ตี~</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Abort" xml:space="preserve" approved="no"> - <source>Abort</source> - <target state="translated">ยกเลิก</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Abort changing address?" xml:space="preserve" approved="no"> - <source>Abort changing address?</source> - <target state="translated">ยกเลิกการเปลี่ยนที่อยู่?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to send files and media." xml:space="preserve" approved="no"> - <source>Allow to send files and media.</source> - <target state="translated">อนุญาตให้ส่งไฟล์และสื่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Abort changing address" xml:space="preserve" approved="no"> - <source>Abort changing address</source> - <target state="translated">ยกเลิกการเปลี่ยนที่อยู่</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no"> - <source>Address change will be aborted. Old receiving address will be used.</source> - <target state="translated">การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Favorite" xml:space="preserve" approved="no"> - <source>Favorite</source> - <target state="translated">ที่ชอบ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Files and media" xml:space="preserve" approved="no"> - <source>Files and media</source> - <target state="translated">ไฟล์และสื่อ</target> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Files and media prohibited!" xml:space="preserve" approved="no"> - <source>Files and media prohibited!</source> - <target state="translated">ไฟล์และสื่อต้องห้าม!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send files and media." xml:space="preserve" approved="no"> - <source>Group members can send files and media.</source> - <target state="translated">สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error aborting address change" xml:space="preserve" approved="no"> - <source>Error aborting address change</source> - <target state="translated">เกิดข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Files and media are prohibited in this group." xml:space="preserve" approved="no"> - <source>Files and media are prohibited in this group.</source> - <target state="translated">ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No filtered chats" xml:space="preserve" approved="no"> - <source>No filtered chats</source> - <target state="translated">ไม่มีการกรองการแชท</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can enable files and media." xml:space="preserve" approved="no"> - <source>Only group owners can enable files and media.</source> - <target state="translated">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานไฟล์และสื่อได้</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve" approved="no"> - <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> - <target state="translated">ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending files and media." xml:space="preserve" approved="no"> - <source>Prohibit sending files and media.</source> - <target state="translated">ห้ามส่งไฟล์และสื่อ</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve" approved="no"> - <source>You can hide or mute a user profile - swipe it to the right.</source> - <target state="translated">คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve" approved="no"> - <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> - <target state="translated">คุณลักษณะนี้เป็นการทดลอง! จะใช้งานได้ก็ต่อเมื่อลูกค้าอื่นติดตั้งเวอร์ชัน 4.2 คุณควรเห็นข้อความในการสนทนาเมื่อการเปลี่ยนแปลงที่อยู่เสร็จสิ้น – โปรดตรวจสอบว่าคุณยังคงสามารถรับข้อความจากผู้ติดต่อนี้ (หรือสมาชิกในกลุ่ม)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unfav." xml:space="preserve" approved="no"> - <source>Unfav.</source> - <target state="translated">เลิกชอบ</target> + <target>\~ตี~</target> <note>No comment provided by engineer.</note> </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> - <trans-unit id="CFBundleName" xml:space="preserve" approved="no"> + <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX</source> - <target state="translated">SimpleX</target> + <target>SimpleX</target> <note>Bundle name</note> </trans-unit> - <trans-unit id="NSCameraUsageDescription" xml:space="preserve" approved="no"> + <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> - <target state="translated">SimpleX ต้องการการเข้าถึงกล้องเพื่อสแกนรหัสคิวอาร์เพื่อเชื่อมต่อกับผู้ใช้รายอื่นและสำหรับการโทรวิดีโอ</target> + <target>SimpleX ต้องการการเข้าถึงกล้องเพื่อสแกนรหัสคิวอาร์เพื่อเชื่อมต่อกับผู้ใช้รายอื่นและสำหรับการโทรวิดีโอ</target> <note>Privacy - Camera Usage Description</note> </trans-unit> - <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve" approved="no"> + <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> - <target state="translated">SimpleX ใช้ Face ID สำหรับการรับรองความถูกต้องในเครื่อง</target> + <target>SimpleX ใช้ Face ID สำหรับการรับรองความถูกต้องในเครื่อง</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> - <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve" approved="no"> + <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> - <target state="translated">SimpleX ต้องการการเข้าถึงไมโครโฟนสำหรับการโทรด้วยเสียงและวิดีโอ และเพื่อบันทึกข้อความเสียง</target> + <target>SimpleX ต้องการการเข้าถึงไมโครโฟนสำหรับการโทรด้วยเสียงและวิดีโอ และเพื่อบันทึกข้อความเสียง</target> <note>Privacy - Microphone Usage Description</note> </trans-unit> - <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve" approved="no"> + <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> - <target state="translated">SimpleX ต้องการเข้าถึง Photo Library เพื่อบันทึกสื่อที่ถ่ายและได้รับ</target> + <target>SimpleX ต้องการเข้าถึง Photo Library เพื่อบันทึกสื่อที่ถ่ายและได้รับ</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="th" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> - <trans-unit id="CFBundleDisplayName" xml:space="preserve" approved="no"> + <trans-unit id="CFBundleDisplayName" xml:space="preserve"> <source>SimpleX NSE</source> - <target state="translated">SimpleX NSE</target> + <target>SimpleX NSE</target> <note>Bundle display name</note> </trans-unit> - <trans-unit id="CFBundleName" xml:space="preserve" approved="no"> + <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX NSE</source> - <target state="translated">SimpleX NSE</target> + <target>SimpleX NSE</target> <note>Bundle name</note> </trans-unit> - <trans-unit id="NSHumanReadableCopyright" xml:space="preserve" approved="no"> + <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> - <target state="translated">ลิขสิทธิ์ © 2022 SimpleX Chat สงวนลิขสิทธิ์</target> + <target>ลิขสิทธิ์ © 2022 SimpleX Chat สงวนลิขสิทธิ์</target> <note>Copyright (human-readable)</note> </trans-unit> </body> diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..124ddbcc33 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cf485752ea --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,30 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; + diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..3af673b19f --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,10 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/th.xcloc/contents.json b/apps/ios/SimpleX Localizations/th.xcloc/contents.json new file mode 100644 index 0000000000..b60f9edb3e --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "th", + "toolInfo" : { + "toolBuildNumber" : "15A240d", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "15.0" + }, + "version" : "1.0" +} \ No newline at end of file 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 1a4e0931ba..4b2ad1548a 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -26,176 +26,218 @@ <source> (</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve"> + <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> <source> (can be copied)</source> + <target state="translated"> (kopyalanabilir)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve"> + <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> <source>!1 colored!</source> + <target state="translated">!1 renkli!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve"> + <trans-unit id="#secret#" xml:space="preserve" approved="no"> <source>#secret#</source> + <target state="translated">#gizli#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve"> + <trans-unit id="%@" xml:space="preserve" approved="no"> <source>%@</source> + <target state="translated">%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve"> + <trans-unit id="%@ %@" xml:space="preserve" approved="no"> <source>%@ %@</source> + <target state="translated">%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ (current)" xml:space="preserve"> + <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> <source>%@ (current)</source> + <target state="translated">%@ (güncel)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ (current):" xml:space="preserve"> + <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> <source>%@ (current):</source> + <target state="translated">%@ (güncel):</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve"> + <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> <source>%@ / %@</source> + <target state="translated">%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> <note>copied message info, <sender> at <time></note> </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve"> + <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> <source>%@ is connected!</source> + <target state="translated">%@ bağlandı!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve"> + <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> <source>%@ is not verified</source> + <target state="translated">%@ onaylanmadı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve"> + <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> <source>%@ is verified</source> + <target state="translated">%@ onaylandı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve"> + <trans-unit id="%@ servers" xml:space="preserve" approved="no"> <source>%@ servers</source> + <target state="translated">%@ sunucuları</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve"> + <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> <source>%@ wants to connect!</source> + <target state="translated">%@ bağlanmak istiyor!</target> <note>notification title</note> </trans-unit> - <trans-unit id="%@:" xml:space="preserve"> + <trans-unit id="%@:" xml:space="preserve" approved="no"> <source>%@:</source> + <target state="translated">%@:</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%d days" xml:space="preserve"> + <trans-unit id="%d days" xml:space="preserve" approved="no"> <source>%d days</source> + <target state="translated">%d gün</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d hours" xml:space="preserve"> + <trans-unit id="%d hours" xml:space="preserve" approved="no"> <source>%d hours</source> + <target state="translated">%d saat</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d min" xml:space="preserve"> + <trans-unit id="%d min" xml:space="preserve" approved="no"> <source>%d min</source> + <target state="translated">%d dakika</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d months" xml:space="preserve"> + <trans-unit id="%d months" xml:space="preserve" approved="no"> <source>%d months</source> + <target state="translated">%d ay</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d sec" xml:space="preserve"> + <trans-unit id="%d sec" xml:space="preserve" approved="no"> <source>%d sec</source> + <target state="translated">%d saniye</target> <note>time interval</note> </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve"> + <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> <source>%d skipped message(s)</source> + <target state="translated">%d okunmamış mesaj(lar)</target> <note>integrity error chat item</note> </trans-unit> - <trans-unit id="%d weeks" xml:space="preserve"> + <trans-unit id="%d weeks" xml:space="preserve" approved="no"> <source>%d weeks</source> + <target state="translated">%d hafta</target> <note>time interval</note> </trans-unit> - <trans-unit id="%lld" xml:space="preserve"> + <trans-unit id="%lld" xml:space="preserve" approved="no"> <source>%lld</source> + <target state="translated">%lld</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve"> + <trans-unit id="%lld %@" xml:space="preserve" approved="no"> <source>%lld %@</source> + <target state="translated">%lld %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve"> + <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> <source>%lld contact(s) selected</source> + <target state="translated">%lld kişi seçildi</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> <source>%lld file(s) with total size of %@</source> + <target state="translated">%lld dosya , toplam büyüklüğü %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld members" xml:space="preserve"> + <trans-unit id="%lld members" xml:space="preserve" approved="no"> <source>%lld members</source> + <target state="translated">%lld üyeler</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve"> + <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> <source>%lld minutes</source> + <target state="translated">%lld dakika</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve"> + <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> <source>%lld second(s)</source> + <target state="translated">%lld saniye</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve"> + <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> <source>%lld seconds</source> + <target state="translated">%lld saniye</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldd" xml:space="preserve"> + <trans-unit id="%lldd" xml:space="preserve" approved="no"> <source>%lldd</source> + <target state="translated">%lldd</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldh" xml:space="preserve"> + <trans-unit id="%lldh" xml:space="preserve" approved="no"> <source>%lldh</source> + <target state="translated">%lldh</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldk" xml:space="preserve"> + <trans-unit id="%lldk" xml:space="preserve" approved="no"> <source>%lldk</source> + <target state="translated">%lldk</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldm" xml:space="preserve"> + <trans-unit id="%lldm" xml:space="preserve" approved="no"> <source>%lldm</source> + <target state="translated">%lldm</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve"> + <trans-unit id="%lldmth" xml:space="preserve" approved="no"> <source>%lldmth</source> + <target state="translated">%lldmth</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%llds" xml:space="preserve"> + <trans-unit id="%llds" xml:space="preserve" approved="no"> <source>%llds</source> + <target state="translated">%llds</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lldw" xml:space="preserve"> + <trans-unit id="%lldw" xml:space="preserve" approved="no"> <source>%lldw</source> + <target state="translated">%lldw</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> <source>%u messages failed to decrypt.</source> + <target state="translated">%u mesaj deşifrelenememektedir.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve"> + <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> <source>%u messages skipped.</source> + <target state="translated">%u mesaj atlandı</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="(" xml:space="preserve"> + <trans-unit id="(" xml:space="preserve" approved="no"> <source>(</source> + <target state="translated">(</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=")" xml:space="preserve"> + <trans-unit id=")" xml:space="preserve" approved="no"> <source>)</source> + <target state="translated">)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> + <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> + <target state="translated">**Yeni kişi ekleyin**: tek seferlik QR Kodunuzu oluşturmak veya kişisel ulaşım bilgileri bağlantısı için.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> + <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> <source>**Create link / QR code** for your contact to use.</source> + <target state="translated">Kişisel kullanım için **Bağlantı / QR Kodu**.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> @@ -437,8 +479,9 @@ <source>All data is erased when it is entered.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve"> + <trans-unit id="All group members will remain connected." xml:space="preserve" approved="no"> <source>All group members will remain connected.</source> + <target state="translated">Tüm grup üyeleri bağlı kalacaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> @@ -477,8 +520,9 @@ <source>Allow message reactions.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> <source>Allow sending direct messages to members.</source> + <target state="translated">Üyelere direkt mesaj göndermeye izin ver.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> @@ -1286,8 +1330,9 @@ <source>Direct messages</source> <note>chat feature</note> </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> + <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> <source>Direct messages between members are prohibited in this group.</source> + <target state="translated">Bu grupta üyeler arasında direkt mesajlaşma yasaktır.</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -2174,12 +2219,14 @@ <source>Learn more</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave" xml:space="preserve"> + <trans-unit id="Leave" xml:space="preserve" approved="no"> <source>Leave</source> + <target state="translated">Ayrıl</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Leave group" xml:space="preserve"> + <trans-unit id="Leave group" xml:space="preserve" approved="no"> <source>Leave group</source> + <target state="translated">Gruptan ayrıl</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Leave group?" xml:space="preserve"> @@ -4582,28 +4629,34 @@ SimpleX servers cannot see your profile.</source> <source>connected</source> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="message received" xml:space="preserve"> + <trans-unit id="message received" xml:space="preserve" approved="no"> <source>message received</source> + <target state="translated">mesaj alındı</target> <note>notification</note> </trans-unit> - <trans-unit id="minutes" xml:space="preserve"> + <trans-unit id="minutes" xml:space="preserve" approved="no"> <source>minutes</source> + <target state="translated">dakikalar</target> <note>time unit</note> </trans-unit> - <trans-unit id="missed call" xml:space="preserve"> + <trans-unit id="missed call" xml:space="preserve" approved="no"> <source>missed call</source> + <target state="translated">cevapsız arama</target> <note>call status</note> </trans-unit> - <trans-unit id="moderated" xml:space="preserve"> + <trans-unit id="moderated" xml:space="preserve" approved="no"> <source>moderated</source> + <target state="needs-translation">moderated</target> <note>moderated chat item</note> </trans-unit> - <trans-unit id="moderated by %@" xml:space="preserve"> + <trans-unit id="moderated by %@" xml:space="preserve" approved="no"> <source>moderated by %@</source> + <target state="translated">%@ tarafından yönetilmekte</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="months" xml:space="preserve"> + <trans-unit id="months" xml:space="preserve" approved="no"> <source>months</source> + <target state="translated">aylar</target> <note>time unit</note> </trans-unit> <trans-unit id="never" xml:space="preserve"> @@ -4695,120 +4748,149 @@ SimpleX servers cannot see your profile.</source> <source>secret</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="security code changed" xml:space="preserve"> + <trans-unit id="security code changed" xml:space="preserve" approved="no"> <source>security code changed</source> + <target state="translated">güvenlik kodu değiştirildi</target> <note>chat item text</note> </trans-unit> - <trans-unit id="starting…" xml:space="preserve"> + <trans-unit id="starting…" xml:space="preserve" approved="no"> <source>starting…</source> + <target state="translated">başlıyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="strike" xml:space="preserve"> + <trans-unit id="strike" xml:space="preserve" approved="no"> <source>strike</source> + <target state="needs-translation">strike</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="this contact" xml:space="preserve"> + <trans-unit id="this contact" xml:space="preserve" approved="no"> <source>this contact</source> + <target state="translated">Bu kişi</target> <note>notification title</note> </trans-unit> - <trans-unit id="unknown" xml:space="preserve"> + <trans-unit id="unknown" xml:space="preserve" approved="no"> <source>unknown</source> + <target state="translated">bilinmeyen</target> <note>connection info</note> </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve"> + <trans-unit id="updated group profile" xml:space="preserve" approved="no"> <source>updated group profile</source> + <target state="translated">grup profili güncellendi</target> <note>rcv group event chat item</note> </trans-unit> - <trans-unit id="v%@ (%@)" xml:space="preserve"> + <trans-unit id="v%@ (%@)" xml:space="preserve" approved="no"> <source>v%@ (%@)</source> + <target state="translated">v%@ (%@)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve"> + <trans-unit id="via contact address link" xml:space="preserve" approved="no"> <source>via contact address link</source> + <target state="translated">bağlantı adres uzantısı ile</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via group link" xml:space="preserve"> + <trans-unit id="via group link" xml:space="preserve" approved="no"> <source>via group link</source> + <target state="translated">grup bağlantısı ile</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve"> + <trans-unit id="via one-time link" xml:space="preserve" approved="no"> <source>via one-time link</source> + <target state="translated">tek kullanımlık bağlantısı ile</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="via relay" xml:space="preserve"> + <trans-unit id="via relay" xml:space="preserve" approved="no"> <source>via relay</source> + <target state="needs-translation">via relay</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> + <trans-unit id="video call (not e2e encrypted)" xml:space="preserve" approved="no"> <source>video call (not e2e encrypted)</source> + <target state="translated">Görüntülü arama (şifrelenmiş değil)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve"> + <trans-unit id="waiting for answer…" xml:space="preserve" approved="no"> <source>waiting for answer…</source> + <target state="translated">cevap bekleniyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve"> + <trans-unit id="waiting for confirmation…" xml:space="preserve" approved="no"> <source>waiting for confirmation…</source> + <target state="translated">onay bekleniyor…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve"> + <trans-unit id="wants to connect to you!" xml:space="preserve" approved="no"> <source>wants to connect to you!</source> + <target state="translated">bağlanmak istiyor!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="weeks" xml:space="preserve"> + <trans-unit id="weeks" xml:space="preserve" approved="no"> <source>weeks</source> + <target state="translated">haftalar</target> <note>time unit</note> </trans-unit> - <trans-unit id="yes" xml:space="preserve"> + <trans-unit id="yes" xml:space="preserve" approved="no"> <source>yes</source> + <target state="translated">evet</target> <note>pref value</note> </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve"> + <trans-unit id="you are invited to group" xml:space="preserve" approved="no"> <source>you are invited to group</source> + <target state="translated">gruba davet edildiniz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you are observer" xml:space="preserve"> + <trans-unit id="you are observer" xml:space="preserve" approved="no"> <source>you are observer</source> + <target state="translated">gözlemcisiniz</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="you changed address" xml:space="preserve"> + <trans-unit id="you changed address" xml:space="preserve" approved="no"> <source>you changed address</source> + <target state="translated">adresinizi değiştirdiniz</target> <note>chat item text</note> </trans-unit> - <trans-unit id="you changed address for %@" xml:space="preserve"> + <trans-unit id="you changed address for %@" xml:space="preserve" approved="no"> <source>you changed address for %@</source> + <target state="translated">adresinizi %@ ile değiştirdiniz</target> <note>chat item text</note> </trans-unit> - <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> + <trans-unit id="you changed role for yourself to %@" xml:space="preserve" approved="no"> <source>you changed role for yourself to %@</source> + <target state="translated">kişisel yetkinizi %@ olarak değiştirdiniz</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> + <trans-unit id="you changed role of %@ to %@" xml:space="preserve" approved="no"> <source>you changed role of %1$@ to %2$@</source> + <target state="translated">%1$@'in yetkisini %2$@ olarak değiştirdiniz</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you left" xml:space="preserve"> + <trans-unit id="you left" xml:space="preserve" approved="no"> <source>you left</source> + <target state="translated">terk ettiniz</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you removed %@" xml:space="preserve"> + <trans-unit id="you removed %@" xml:space="preserve" approved="no"> <source>you removed %@</source> + <target state="translated">%@'yi çıkarttınız</target> <note>snd group event chat item</note> </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve"> + <trans-unit id="you shared one-time link" xml:space="preserve" approved="no"> <source>you shared one-time link</source> + <target state="translated">tek kullanımlık bağlantınızı paylaştınız</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="you shared one-time link incognito" xml:space="preserve"> + <trans-unit id="you shared one-time link incognito" xml:space="preserve" approved="no"> <source>you shared one-time link incognito</source> + <target state="translated">tek kullanımlık link paylaştınız gizli</target> <note>chat list item description</note> </trans-unit> - <trans-unit id="you: " xml:space="preserve"> + <trans-unit id="you: " xml:space="preserve" approved="no"> <source>you: </source> + <target state="translated">sen: </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="~strike~" xml:space="preserve"> + <trans-unit id="~strike~" xml:space="preserve" approved="no"> <source>\~strike~</source> + <target state="needs-translation">\~strike~</target> <note>No comment provided by engineer.</note> </trans-unit> </body> @@ -4818,8 +4900,9 @@ SimpleX servers cannot see your profile.</source> <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> </header> <body> - <trans-unit id="CFBundleName" xml:space="preserve"> + <trans-unit id="CFBundleName" xml:space="preserve" approved="no"> <source>SimpleX</source> + <target state="translated">SimpleX</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..4b8ee6308e --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,15 @@ +{ + "colors" : [ + { + "idiom" : "universal", + "locale" : "uk" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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 434b19a6de..4947bf80c5 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2,5523 +2,6304 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="uk" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" " xml:space="preserve" approved="no"> + <trans-unit id=" " xml:space="preserve"> <source> </source> - <target state="translated"> </target> + <target> </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (" xml:space="preserve" approved="no"> + <trans-unit id=" (" xml:space="preserve"> <source> (</source> - <target state="translated"> (</target> + <target> (</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id=" (can be copied)" xml:space="preserve" approved="no"> + <trans-unit id=" (can be copied)" xml:space="preserve"> <source> (can be copied)</source> - <target state="translated"> (можна скопіювати)</target> + <target> (можна скопіювати)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="!1 colored!" xml:space="preserve" approved="no"> + <trans-unit id="!1 colored!" xml:space="preserve"> <source>!1 colored!</source> - <target state="translated">!1 кольоровий!</target> + <target>!1 кольоровий!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="#secret#" xml:space="preserve" approved="no"> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## Історія</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## У відповідь на</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> - <target state="translated">#секрет#</target> + <target>#секрет#</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@" xml:space="preserve" approved="no"> + <trans-unit id="%@" xml:space="preserve"> <source>%@</source> - <target state="translated">%@</target> + <target>%@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ %@" xml:space="preserve" approved="no"> + <trans-unit id="%@ %@" xml:space="preserve"> <source>%@ %@</source> - <target state="translated">%@ %@</target> + <target>%@ %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ / %@" xml:space="preserve" approved="no"> - <source>%@ / %@</source> - <target state="translated">%@ / %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ is connected!" xml:space="preserve" approved="no"> - <source>%@ is connected!</source> - <target state="translated">%@ підключено!</target> - <note>notification title</note> - </trans-unit> - <trans-unit id="%@ is not verified" xml:space="preserve" approved="no"> - <source>%@ is not verified</source> - <target state="translated">%@ не перевірено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ is verified" xml:space="preserve" approved="no"> - <source>%@ is verified</source> - <target state="translated">%@ перевірено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ wants to connect!" xml:space="preserve" approved="no"> - <source>%@ wants to connect!</source> - <target state="translated">%@ хоче підключитися!</target> - <note>notification title</note> - </trans-unit> - <trans-unit id="%d days" xml:space="preserve" approved="no"> - <source>%d days</source> - <target state="translated">%d днів</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d hours" xml:space="preserve" approved="no"> - <source>%d hours</source> - <target state="translated">%d годин</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d min" xml:space="preserve" approved="no"> - <source>%d min</source> - <target state="translated">%d хв</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d months" xml:space="preserve" approved="no"> - <source>%d months</source> - <target state="translated">%d місяців</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d sec" xml:space="preserve" approved="no"> - <source>%d sec</source> - <target state="translated">%d сек</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="%d skipped message(s)" xml:space="preserve" approved="no"> - <source>%d skipped message(s)</source> - <target state="translated">%d пропущено повідомлення(ь)</target> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="%lld" xml:space="preserve" approved="no"> - <source>%lld</source> - <target state="translated">%lld</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld %@" xml:space="preserve" approved="no"> - <source>%lld %@</source> - <target state="translated">%lld %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld contact(s) selected" xml:space="preserve" approved="no"> - <source>%lld contact(s) selected</source> - <target state="translated">%lld контакт(и) вибрані</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve" approved="no"> - <source>%lld file(s) with total size of %@</source> - <target state="translated">%lld файл(и) загальним розміром %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld members" xml:space="preserve" approved="no"> - <source>%lld members</source> - <target state="translated">%lld учасників</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld second(s)" xml:space="preserve" approved="no"> - <source>%lld second(s)</source> - <target state="translated">%lld секунд(и)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldd" xml:space="preserve" approved="no"> - <source>%lldd</source> - <target state="translated">%lldd</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldh" xml:space="preserve" approved="no"> - <source>%lldh</source> - <target state="translated">%lldh</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldk" xml:space="preserve" approved="no"> - <source>%lldk</source> - <target state="translated">%lldk</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldm" xml:space="preserve" approved="no"> - <source>%lldm</source> - <target state="translated">%lldm</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldmth" xml:space="preserve" approved="no"> - <source>%lldmth</source> - <target state="translated">%lldmth</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%llds" xml:space="preserve" approved="no"> - <source>%llds</source> - <target state="translated">%llds</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lldw" xml:space="preserve" approved="no"> - <source>%lldw</source> - <target state="translated">%lldw</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="(" xml:space="preserve" approved="no"> - <source>(</source> - <target state="translated">(</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=")" xml:space="preserve" approved="no"> - <source>)</source> - <target state="translated">)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve" approved="no"> - <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> - <target state="translated">**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve" approved="no"> - <source>**Create link / QR code** for your contact to use.</source> - <target state="translated">**Створіть посилання / QR-код** для використання вашим контактом.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve" approved="no"> - <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> - <target state="translated">**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve" approved="no"> - <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> - <target state="translated">**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve" approved="no"> - <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> - <target state="translated">**Вставте отримане посилання** або відкрийте його в браузері і натисніть **Відкрити в мобільному додатку**.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve" approved="no"> - <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> - <target state="translated">**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve" approved="no"> - <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> - <target state="translated">**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve" approved="no"> - <source>**Scan QR code**: to connect to your contact in person or via video call.</source> - <target state="translated">**Відскануйте QR-код**: щоб з'єднатися з вашим контактом особисто або за допомогою відеодзвінка.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve" approved="no"> - <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> - <target state="translated">**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**e2e encrypted** audio call" xml:space="preserve" approved="no"> - <source>**e2e encrypted** audio call</source> - <target state="translated">**e2e encrypted** аудіодзвінок</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="**e2e encrypted** video call" xml:space="preserve" approved="no"> - <source>**e2e encrypted** video call</source> - <target state="translated">**e2e encrypted** відеодзвінок</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="*bold*" xml:space="preserve" approved="no"> - <source>\*bold*</source> - <target state="translated">\*жирний*</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=", " xml:space="preserve" approved="no"> - <source>, </source> - <target state="translated">, </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="." xml:space="preserve" approved="no"> - <source>.</source> - <target state="translated">.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="1 day" xml:space="preserve" approved="no"> - <source>1 day</source> - <target state="translated">1 день</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="1 hour" xml:space="preserve" approved="no"> - <source>1 hour</source> - <target state="translated">1 година</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="1 month" xml:space="preserve" approved="no"> - <source>1 month</source> - <target state="translated">1 місяць</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="1 week" xml:space="preserve" approved="no"> - <source>1 week</source> - <target state="translated">1 тиждень</target> - <note>message ttl</note> - </trans-unit> - <trans-unit id="2 weeks" xml:space="preserve"> - <source>2 weeks</source> - <note>message ttl</note> - </trans-unit> - <trans-unit id="6" xml:space="preserve" approved="no"> - <source>6</source> - <target state="translated">6</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id=": " xml:space="preserve" approved="no"> - <source>: </source> - <target state="translated">: </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A new contact" xml:space="preserve" approved="no"> - <source>A new contact</source> - <target state="translated">Новий контакт</target> - <note>notification title</note> - </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve" approved="no"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target state="translated">Випадковий профіль буде надіслано контакту, від якого ви отримали це посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve" approved="no"> - <source>A random profile will be sent to your contact</source> - <target state="translated">Випадковий профіль буде надіслано на ваш контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve" approved="no"> - <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> - <target state="translated">Для кожного профілю чату, який ви маєте в додатку, буде використовуватися окреме TCP-з'єднання.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve" approved="no"> - <source>A separate TCP connection will be used **for each contact and group member**. -**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> - <target state="translated">Для кожного контакту та учасника групи буде використовуватися окреме TCP-з'єднання. -**Зверніть увагу: якщо у вас багато з'єднань, споживання заряду акумулятора і трафіку може бути значно вищим, а деякі з'єднання можуть обірватися.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX" xml:space="preserve" approved="no"> - <source>About SimpleX</source> - <target state="translated">Про SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="About SimpleX Chat" xml:space="preserve" approved="no"> - <source>About SimpleX Chat</source> - <target state="translated">Про чат SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accent color" xml:space="preserve" approved="no"> - <source>Accent color</source> - <target state="translated">Акцентний колір</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept" xml:space="preserve" approved="no"> - <source>Accept</source> - <target state="translated">Прийняти</target> - <note>accept contact request via notification - accept incoming call via notification</note> - </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve" approved="no"> - <source>Accept contact</source> - <target state="translated">Прийняти контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept contact request from %@?" xml:space="preserve" approved="no"> - <source>Accept contact request from %@?</source> - <target state="translated">Прийняти запит на контакт від %@?</target> - <note>notification body</note> - </trans-unit> - <trans-unit id="Accept incognito" xml:space="preserve" approved="no"> - <source>Accept incognito</source> - <target state="translated">Прийняти інкогніто</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Accept requests" xml:space="preserve"> - <source>Accept requests</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add preset servers" xml:space="preserve" approved="no"> - <source>Add preset servers</source> - <target state="translated">Додавання попередньо встановлених серверів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add profile" xml:space="preserve" approved="no"> - <source>Add profile</source> - <target state="translated">Додати профіль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add servers by scanning QR codes." xml:space="preserve" approved="no"> - <source>Add servers by scanning QR codes.</source> - <target state="translated">Додайте сервери, відсканувавши QR-код.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add server…" xml:space="preserve" approved="no"> - <source>Add server…</source> - <target state="translated">Додати сервер…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Add to another device" xml:space="preserve" approved="no"> - <source>Add to another device</source> - <target state="translated">Додати до іншого пристрою</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Admins can create the links to join groups." xml:space="preserve" approved="no"> - <source>Admins can create the links to join groups.</source> - <target state="translated">Адміни можуть створювати посилання для приєднання до груп.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Advanced network settings" xml:space="preserve" approved="no"> - <source>Advanced network settings</source> - <target state="translated">Розширені налаштування мережі</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> - <source>All chats and messages will be deleted - this cannot be undone!</source> - <target state="translated">Всі чати та повідомлення будуть видалені - це неможливо скасувати!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All group members will remain connected." xml:space="preserve" approved="no"> - <source>All group members will remain connected.</source> - <target state="translated">Всі учасники групи залишаться на зв'язку.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve" approved="no"> - <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> - <target state="translated">Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="All your contacts will remain connected" xml:space="preserve"> - <source>All your contacts will remain connected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow" xml:space="preserve" approved="no"> - <source>Allow</source> - <target state="translated">Дозволити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve" approved="no"> - <source>Allow disappearing messages only if your contact allows it to you.</source> - <target state="translated">Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve" approved="no"> - <source>Allow irreversible message deletion only if your contact allows it to you.</source> - <target state="translated">Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow sending direct messages to members." xml:space="preserve" approved="no"> - <source>Allow sending direct messages to members.</source> - <target state="translated">Дозволяє надсилати прямі повідомлення користувачам.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow sending disappearing messages." xml:space="preserve" approved="no"> - <source>Allow sending disappearing messages.</source> - <target state="translated">Дозволити надсилання зникаючих повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve" approved="no"> - <source>Allow to irreversibly delete sent messages.</source> - <target state="translated">Дозволяє безповоротно видаляти надіслані повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow to send voice messages." xml:space="preserve" approved="no"> - <source>Allow to send voice messages.</source> - <target state="translated">Дозволити надсилати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve" approved="no"> - <source>Allow voice messages only if your contact allows them.</source> - <target state="translated">Дозволяйте голосові повідомлення, тільки якщо ваш контакт дозволяє їх.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow voice messages?" xml:space="preserve" approved="no"> - <source>Allow voice messages?</source> - <target state="translated">Дозволити голосові повідомлення?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve" approved="no"> - <source>Allow your contacts to irreversibly delete sent messages.</source> - <target state="translated">Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve" approved="no"> - <source>Allow your contacts to send disappearing messages.</source> - <target state="translated">Дозвольте своїм контактам надсилати зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve" approved="no"> - <source>Allow your contacts to send voice messages.</source> - <target state="translated">Дозвольте своїм контактам надсилати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Already connected?" xml:space="preserve" approved="no"> - <source>Already connected?</source> - <target state="translated">Вже підключено?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Always use relay" xml:space="preserve" approved="no"> - <source>Always use relay</source> - <target state="translated">Завжди використовуйте реле</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Answer call" xml:space="preserve" approved="no"> - <source>Answer call</source> - <target state="translated">Відповісти на дзвінок</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App build: %@" xml:space="preserve" approved="no"> - <source>App build: %@</source> - <target state="translated">Збірка програми: %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App icon" xml:space="preserve" approved="no"> - <source>App icon</source> - <target state="translated">Іконка програми</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App version" xml:space="preserve" approved="no"> - <source>App version</source> - <target state="translated">Версія програми</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App version: v%@" xml:space="preserve" approved="no"> - <source>App version: v%@</source> - <target state="translated">Версія програми: v%@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Appearance" xml:space="preserve" approved="no"> - <source>Appearance</source> - <target state="translated">Зовнішній вигляд</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Attach" xml:space="preserve" approved="no"> - <source>Attach</source> - <target state="translated">Прикріпити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Audio & video calls" xml:space="preserve" approved="no"> - <source>Audio & video calls</source> - <target state="translated">Аудіо та відео дзвінки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication failed" xml:space="preserve" approved="no"> - <source>Authentication failed</source> - <target state="translated">Не вдалося пройти автентифікацію</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve" approved="no"> - <source>Authentication is required before the call is connected, but you may miss calls.</source> - <target state="translated">Перед з'єднанням дзвінка потрібно пройти автентифікацію, але ви можете пропустити дзвінки.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication unavailable" xml:space="preserve" approved="no"> - <source>Authentication unavailable</source> - <target state="translated">Автентифікація недоступна</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept contact requests" xml:space="preserve" approved="no"> - <source>Auto-accept contact requests</source> - <target state="translated">Автоматичне прийняття запитів на контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Auto-accept images" xml:space="preserve" approved="no"> - <source>Auto-accept images</source> - <target state="translated">Автоматичне прийняття зображень</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Automatically" xml:space="preserve"> - <source>Automatically</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Back" xml:space="preserve" approved="no"> - <source>Back</source> - <target state="translated">Назад</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve" approved="no"> - <source>Both you and your contact can irreversibly delete sent messages.</source> - <target state="translated">І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve" approved="no"> - <source>Both you and your contact can send disappearing messages.</source> - <target state="translated">Ви і ваш контакт можете надсилати зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve" approved="no"> - <source>Both you and your contact can send voice messages.</source> - <target state="translated">Надсилати голосові повідомлення можете як ви, так і ваш контакт.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve" approved="no"> - <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> - <target state="translated">Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Call already ended!" xml:space="preserve" approved="no"> - <source>Call already ended!</source> - <target state="translated">Дзвінок вже закінчився!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Calls" xml:space="preserve" approved="no"> - <source>Calls</source> - <target state="translated">Дзвінки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't invite contact!" xml:space="preserve" approved="no"> - <source>Can't invite contact!</source> - <target state="translated">Не вдається запросити контакт!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Can't invite contacts!" xml:space="preserve" approved="no"> - <source>Can't invite contacts!</source> - <target state="translated">Неможливо запросити контакти!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cancel" xml:space="preserve" approved="no"> - <source>Cancel</source> - <target state="translated">Скасувати</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cannot access keychain to save database password" xml:space="preserve" approved="no"> - <source>Cannot access keychain to save database password</source> - <target state="translated">Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Cannot receive file" xml:space="preserve" approved="no"> - <source>Cannot receive file</source> - <target state="translated">Не вдається отримати файл</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change" xml:space="preserve" approved="no"> - <source>Change</source> - <target state="translated">Зміна</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change database passphrase?" xml:space="preserve" approved="no"> - <source>Change database passphrase?</source> - <target state="translated">Змінити пароль до бази даних?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change member role?" xml:space="preserve" approved="no"> - <source>Change member role?</source> - <target state="translated">Змінити роль учасника?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change receiving address" xml:space="preserve" approved="no"> - <source>Change receiving address</source> - <target state="translated">Змінити адресу отримання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change receiving address?" xml:space="preserve" approved="no"> - <source>Change receiving address?</source> - <target state="translated">Змінити адресу отримання?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change role" xml:space="preserve" approved="no"> - <source>Change role</source> - <target state="translated">Змінити роль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat archive" xml:space="preserve" approved="no"> - <source>Chat archive</source> - <target state="translated">Архів чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat console" xml:space="preserve" approved="no"> - <source>Chat console</source> - <target state="translated">Консоль чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database" xml:space="preserve" approved="no"> - <source>Chat database</source> - <target state="translated">База даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database deleted" xml:space="preserve" approved="no"> - <source>Chat database deleted</source> - <target state="translated">Видалено базу даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat database imported" xml:space="preserve" approved="no"> - <source>Chat database imported</source> - <target state="translated">Імпорт бази даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat is running" xml:space="preserve" approved="no"> - <source>Chat is running</source> - <target state="translated">Чат запущено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat is stopped" xml:space="preserve" approved="no"> - <source>Chat is stopped</source> - <target state="translated">Чат зупинено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chat preferences" xml:space="preserve" approved="no"> - <source>Chat preferences</source> - <target state="translated">Налаштування чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Chats" xml:space="preserve" approved="no"> - <source>Chats</source> - <target state="translated">Чати</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Check server address and try again." xml:space="preserve" approved="no"> - <source>Check server address and try again.</source> - <target state="translated">Перевірте адресу сервера та спробуйте ще раз.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Choose file" xml:space="preserve" approved="no"> - <source>Choose file</source> - <target state="translated">Виберіть файл</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Choose from library" xml:space="preserve" approved="no"> - <source>Choose from library</source> - <target state="translated">Виберіть з бібліотеки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear" xml:space="preserve" approved="no"> - <source>Clear</source> - <target state="translated">Чисто</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear conversation" xml:space="preserve" approved="no"> - <source>Clear conversation</source> - <target state="translated">Ясна розмова</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear conversation?" xml:space="preserve" approved="no"> - <source>Clear conversation?</source> - <target state="translated">Відверта розмова?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Clear verification" xml:space="preserve" approved="no"> - <source>Clear verification</source> - <target state="translated">Очистити перевірку</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Colors" xml:space="preserve" approved="no"> - <source>Colors</source> - <target state="translated">Кольори</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Compare security codes with your contacts." xml:space="preserve" approved="no"> - <source>Compare security codes with your contacts.</source> - <target state="translated">Порівняйте коди безпеки зі своїми контактами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Configure ICE servers" xml:space="preserve" approved="no"> - <source>Configure ICE servers</source> - <target state="translated">Налаштування серверів ICE</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm" xml:space="preserve" approved="no"> - <source>Confirm</source> - <target state="translated">Підтвердити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm new passphrase…" xml:space="preserve" approved="no"> - <source>Confirm new passphrase…</source> - <target state="translated">Підтвердіть нову парольну фразу…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect" xml:space="preserve" approved="no"> - <source>Connect</source> - <target state="translated">Підключіться</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve" approved="no"> - <source>Connect via contact link?</source> - <target state="translated">Підключитися за контактним посиланням?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via group link?" xml:space="preserve" approved="no"> - <source>Connect via group link?</source> - <target state="translated">Підключитися за груповим посиланням?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via link" xml:space="preserve" approved="no"> - <source>Connect via link</source> - <target state="translated">Підключіться за посиланням</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via link / QR code" xml:space="preserve" approved="no"> - <source>Connect via link / QR code</source> - <target state="translated">Підключитися за посиланням / QR-кодом</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve" approved="no"> - <source>Connect via one-time link?</source> - <target state="translated">Підключитися за одноразовим посиланням?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connecting server…" xml:space="preserve" approved="no"> - <source>Connecting to server…</source> - <target state="translated">Підключення до сервера…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connecting server… (error: %@)" xml:space="preserve" approved="no"> - <source>Connecting to server… (error: %@)</source> - <target state="translated">Підключення до сервера... (помилка: %@)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection" xml:space="preserve" approved="no"> - <source>Connection</source> - <target state="translated">Підключення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection error" xml:space="preserve" approved="no"> - <source>Connection error</source> - <target state="translated">Помилка підключення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection error (AUTH)" xml:space="preserve" approved="no"> - <source>Connection error (AUTH)</source> - <target state="translated">Помилка підключення (AUTH)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request" xml:space="preserve" approved="no"> - <source>Connection request</source> - <target state="translated">Запит на підключення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection request sent!" xml:space="preserve" approved="no"> - <source>Connection request sent!</source> - <target state="translated">Запит на підключення відправлено!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Connection timeout" xml:space="preserve" approved="no"> - <source>Connection timeout</source> - <target state="translated">Тайм-аут з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact allows" xml:space="preserve" approved="no"> - <source>Contact allows</source> - <target state="translated">Контакт дозволяє</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact already exists" xml:space="preserve" approved="no"> - <source>Contact already exists</source> - <target state="translated">Контакт вже існує</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve" approved="no"> - <source>Contact and all messages will be deleted - this cannot be undone!</source> - <target state="translated">Контакт і всі повідомлення будуть видалені - це неможливо скасувати!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact hidden:" xml:space="preserve" approved="no"> - <source>Contact hidden:</source> - <target state="translated">Контакт приховано:</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Contact is connected" xml:space="preserve" approved="no"> - <source>Contact is connected</source> - <target state="translated">Контакт підключений</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Contact is not connected yet!" xml:space="preserve" approved="no"> - <source>Contact is not connected yet!</source> - <target state="translated">Контакт ще не підключено!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact name" xml:space="preserve" approved="no"> - <source>Contact name</source> - <target state="translated">Ім'я контактної особи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact preferences" xml:space="preserve" approved="no"> - <source>Contact preferences</source> - <target state="translated">Налаштування контактів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contact requests" xml:space="preserve"> - <source>Contact requests</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve" approved="no"> - <source>Contacts can mark messages for deletion; you will be able to view them.</source> - <target state="translated">Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Copy" xml:space="preserve" approved="no"> - <source>Copy</source> - <target state="translated">Копіювати</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Core built at: %@" xml:space="preserve"> - <source>Core built at: %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Core version: v%@" xml:space="preserve" approved="no"> - <source>Core version: v%@</source> - <target state="translated">Основна версія: v%@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create" xml:space="preserve" approved="no"> - <source>Create</source> - <target state="translated">Створити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create address" xml:space="preserve"> - <source>Create address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create group link" xml:space="preserve" approved="no"> - <source>Create group link</source> - <target state="translated">Створити групове посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create link" xml:space="preserve" approved="no"> - <source>Create link</source> - <target state="translated">Створити посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create one-time invitation link" xml:space="preserve" approved="no"> - <source>Create one-time invitation link</source> - <target state="translated">Створіть одноразове посилання-запрошення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create queue" xml:space="preserve" approved="no"> - <source>Create queue</source> - <target state="translated">Створити чергу</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Create secret group" xml:space="preserve" approved="no"> - <source>Create secret group</source> - <target state="translated">Створити секретну групу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create your profile" xml:space="preserve" approved="no"> - <source>Create your profile</source> - <target state="translated">Створіть свій профіль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Created on %@" xml:space="preserve" approved="no"> - <source>Created on %@</source> - <target state="translated">Створено %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Current passphrase…" xml:space="preserve" approved="no"> - <source>Current passphrase…</source> - <target state="translated">Поточна парольна фраза…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve" approved="no"> - <source>Currently maximum supported file size is %@.</source> - <target state="translated">Наразі максимальний підтримуваний розмір файлу - %@.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Dark" xml:space="preserve" approved="no"> - <source>Dark</source> - <target state="translated">Темний</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database ID" xml:space="preserve" approved="no"> - <source>Database ID</source> - <target state="translated">Ідентифікатор бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encrypted!" xml:space="preserve" approved="no"> - <source>Database encrypted!</source> - <target state="translated">База даних зашифрована!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve" approved="no"> - <source>Database encryption passphrase will be updated and stored in the keychain. -</source> - <target state="translated">Парольна фраза шифрування бази даних буде оновлена та збережена у в’язці ключів. -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve" approved="no"> - <source>Database encryption passphrase will be updated. -</source> - <target state="translated">Ключову фразу шифрування бази даних буде оновлено. -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database error" xml:space="preserve" approved="no"> - <source>Database error</source> - <target state="translated">Помилка в базі даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve" approved="no"> - <source>Database is encrypted using a random passphrase, you can change it.</source> - <target state="translated">База даних зашифрована за допомогою випадкової парольної фрази, яку ви можете змінити.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve" approved="no"> - <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> - <target state="translated">База даних зашифрована за допомогою випадкової парольної фрази. Будь ласка, змініть його перед експортом.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase" xml:space="preserve" approved="no"> - <source>Database passphrase</source> - <target state="translated">Ключова фраза бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase & export" xml:space="preserve" approved="no"> - <source>Database passphrase & export</source> - <target state="translated">Ключова фраза бази даних та експорт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve" approved="no"> - <source>Database passphrase is different from saved in the keychain.</source> - <target state="translated">Парольна фраза бази даних відрізняється від збереженої у в’язці ключів.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database passphrase is required to open chat." xml:space="preserve" approved="no"> - <source>Database passphrase is required to open chat.</source> - <target state="translated">Для відкриття чату потрібно ввести пароль до бази даних.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve" approved="no"> - <source>Database will be encrypted and the passphrase stored in the keychain. -</source> - <target state="translated">База даних буде зашифрована, а парольна фраза збережена у в’язці ключів. -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be encrypted. " xml:space="preserve" approved="no"> - <source>Database will be encrypted. -</source> - <target state="translated">База даних буде зашифрована. -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve" approved="no"> - <source>Database will be migrated when the app restarts</source> - <target state="translated">База даних буде перенесена під час перезапуску програми</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Decentralized" xml:space="preserve" approved="no"> - <source>Decentralized</source> - <target state="translated">Децентралізований</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete" xml:space="preserve" approved="no"> - <source>Delete</source> - <target state="translated">Видалити</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Delete Contact" xml:space="preserve" approved="no"> - <source>Delete Contact</source> - <target state="translated">Видалити контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete address" xml:space="preserve" approved="no"> - <source>Delete address</source> - <target state="translated">Видалити адресу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete address?" xml:space="preserve" approved="no"> - <source>Delete address?</source> - <target state="translated">Видалити адресу?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete after" xml:space="preserve" approved="no"> - <source>Delete after</source> - <target state="translated">Видалити після</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete all files" xml:space="preserve" approved="no"> - <source>Delete all files</source> - <target state="translated">Видалити всі файли</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete archive" xml:space="preserve" approved="no"> - <source>Delete archive</source> - <target state="translated">Видалити архів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat archive?" xml:space="preserve" approved="no"> - <source>Delete chat archive?</source> - <target state="translated">Видалити архів чату?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat profile?" xml:space="preserve" approved="no"> - <source>Delete chat profile?</source> - <target state="translated">Видалити профіль чату?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete connection" xml:space="preserve" approved="no"> - <source>Delete connection</source> - <target state="translated">Видалити підключення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete contact" xml:space="preserve" approved="no"> - <source>Delete contact</source> - <target state="translated">Видалити контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete contact?" xml:space="preserve" approved="no"> - <source>Delete contact?</source> - <target state="translated">Видалити контакт?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete database" xml:space="preserve" approved="no"> - <source>Delete database</source> - <target state="translated">Видалити базу даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete files and media?" xml:space="preserve" approved="no"> - <source>Delete files and media?</source> - <target state="translated">Видаляти файли та медіа?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete files for all chat profiles" xml:space="preserve" approved="no"> - <source>Delete files for all chat profiles</source> - <target state="translated">Видалення файлів для всіх профілів чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete for everyone" xml:space="preserve" approved="no"> - <source>Delete for everyone</source> - <target state="translated">Видалити для всіх</target> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Delete for me" xml:space="preserve" approved="no"> - <source>Delete for me</source> - <target state="translated">Видалити для мене</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete group" xml:space="preserve" approved="no"> - <source>Delete group</source> - <target state="translated">Видалити групу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete group?" xml:space="preserve" approved="no"> - <source>Delete group?</source> - <target state="translated">Видалити групу?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete invitation" xml:space="preserve" approved="no"> - <source>Delete invitation</source> - <target state="translated">Видалити запрошення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete link" xml:space="preserve" approved="no"> - <source>Delete link</source> - <target state="translated">Видалити посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete link?" xml:space="preserve" approved="no"> - <source>Delete link?</source> - <target state="translated">Видалити посилання?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete member message?" xml:space="preserve" approved="no"> - <source>Delete member message?</source> - <target state="translated">Видалити повідомлення учасника?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete message?" xml:space="preserve" approved="no"> - <source>Delete message?</source> - <target state="translated">Видалити повідомлення?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete messages" xml:space="preserve" approved="no"> - <source>Delete messages</source> - <target state="translated">Видалити повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete messages after" xml:space="preserve" approved="no"> - <source>Delete messages after</source> - <target state="translated">Видаляйте повідомлення після</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete old database" xml:space="preserve" approved="no"> - <source>Delete old database</source> - <target state="translated">Видалення старої бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete old database?" xml:space="preserve" approved="no"> - <source>Delete old database?</source> - <target state="translated">Видалити стару базу даних?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete pending connection" xml:space="preserve" approved="no"> - <source>Delete pending connection</source> - <target state="translated">Видалити очікуване з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete pending connection?" xml:space="preserve" approved="no"> - <source>Delete pending connection?</source> - <target state="translated">Видалити очікуване з'єднання?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete queue" xml:space="preserve" approved="no"> - <source>Delete queue</source> - <target state="translated">Видалити чергу</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Delete user profile?" xml:space="preserve" approved="no"> - <source>Delete user profile?</source> - <target state="translated">Видалити профіль користувача?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Description" xml:space="preserve" approved="no"> - <source>Description</source> - <target state="translated">Опис</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Develop" xml:space="preserve" approved="no"> - <source>Develop</source> - <target state="translated">Розробник</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Developer tools" xml:space="preserve" approved="no"> - <source>Developer tools</source> - <target state="translated">Інструменти для розробників</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device" xml:space="preserve" approved="no"> - <source>Device</source> - <target state="translated">Пристрій</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve" approved="no"> - <source>Device authentication is disabled. Turning off SimpleX Lock.</source> - <target state="translated">Автентифікацію пристрою вимкнено. Вимкнення SimpleX Lock.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve" approved="no"> - <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> - <target state="translated">Автентифікація пристрою не ввімкнена. Ви можете увімкнути SimpleX Lock у Налаштуваннях, коли увімкнете автентифікацію пристрою.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve" approved="no"> - <source>Different names, avatars and transport isolation.</source> - <target state="translated">Різні імена, аватарки та транспортна ізоляція.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Direct messages" xml:space="preserve" approved="no"> - <source>Direct messages</source> - <target state="translated">Прямі повідомлення</target> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve" approved="no"> - <source>Direct messages between members are prohibited in this group.</source> - <target state="translated">У цій групі заборонені прямі повідомлення між учасниками.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disable SimpleX Lock" xml:space="preserve" approved="no"> - <source>Disable SimpleX Lock</source> - <target state="translated">Вимкнути SimpleX Lock</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Disappearing messages" xml:space="preserve" approved="no"> - <source>Disappearing messages</source> - <target state="translated">Зникаючі повідомлення</target> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve" approved="no"> - <source>Disappearing messages are prohibited in this chat.</source> - <target state="translated">Зникаючі повідомлення в цьому чаті заборонені.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve" approved="no"> - <source>Disappearing messages are prohibited in this group.</source> - <target state="translated">У цій групі заборонено зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Disconnect" xml:space="preserve" approved="no"> - <source>Disconnect</source> - <target state="translated">Від'єднати</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Display name" xml:space="preserve" approved="no"> - <source>Display name</source> - <target state="translated">Відображуване ім'я</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Display name:" xml:space="preserve" approved="no"> - <source>Display name:</source> - <target state="translated">Відображуване ім'я:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve" approved="no"> - <source>Do NOT use SimpleX for emergency calls.</source> - <target state="translated">НЕ використовуйте SimpleX для екстрених викликів.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Do it later" xml:space="preserve" approved="no"> - <source>Do it later</source> - <target state="translated">Зробіть це пізніше</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Duplicate display name!" xml:space="preserve" approved="no"> - <source>Duplicate display name!</source> - <target state="translated">Дублююче ім'я користувача!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Edit" xml:space="preserve" approved="no"> - <source>Edit</source> - <target state="translated">Редагувати</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Edit group profile" xml:space="preserve" approved="no"> - <source>Edit group profile</source> - <target state="translated">Редагування профілю групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable" xml:space="preserve" approved="no"> - <source>Enable</source> - <target state="translated">Увімкнути</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable SimpleX Lock" xml:space="preserve" approved="no"> - <source>Enable SimpleX Lock</source> - <target state="translated">Увімкнути SimpleX Lock</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Enable TCP keep-alive" xml:space="preserve" approved="no"> - <source>Enable TCP keep-alive</source> - <target state="translated">Увімкнути TCP keep-alive</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable automatic message deletion?" xml:space="preserve" approved="no"> - <source>Enable automatic message deletion?</source> - <target state="translated">Увімкнути автоматичне видалення повідомлень?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable instant notifications?" xml:space="preserve" approved="no"> - <source>Enable instant notifications?</source> - <target state="translated">Увімкнути миттєві сповіщення?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable notifications" xml:space="preserve" approved="no"> - <source>Enable notifications</source> - <target state="translated">Увімкнути сповіщення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable periodic notifications?" xml:space="preserve" approved="no"> - <source>Enable periodic notifications?</source> - <target state="translated">Увімкнути періодичні сповіщення?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypt" xml:space="preserve" approved="no"> - <source>Encrypt</source> - <target state="translated">Зашифрувати</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypt database?" xml:space="preserve" approved="no"> - <source>Encrypt database?</source> - <target state="translated">Зашифрувати базу даних?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypted database" xml:space="preserve" approved="no"> - <source>Encrypted database</source> - <target state="translated">Зашифрована база даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Encrypted message or another event" xml:space="preserve" approved="no"> - <source>Encrypted message or another event</source> - <target state="translated">Зашифроване повідомлення або інша подія</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: database error" xml:space="preserve" approved="no"> - <source>Encrypted message: database error</source> - <target state="translated">Зашифроване повідомлення: помилка бази даних</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: keychain error" xml:space="preserve" approved="no"> - <source>Encrypted message: keychain error</source> - <target state="translated">Зашифроване повідомлення: помилка ланцюжка ключів</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: no passphrase" xml:space="preserve" approved="no"> - <source>Encrypted message: no passphrase</source> - <target state="translated">Зашифроване повідомлення: без ключової фрази</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Encrypted message: unexpected error" xml:space="preserve" approved="no"> - <source>Encrypted message: unexpected error</source> - <target state="translated">Зашифроване повідомлення: несподівана помилка</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Enter correct passphrase." xml:space="preserve" approved="no"> - <source>Enter correct passphrase.</source> - <target state="translated">Введіть правильну парольну фразу.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter passphrase…" xml:space="preserve" approved="no"> - <source>Enter passphrase…</source> - <target state="translated">Введіть пароль…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter server manually" xml:space="preserve" approved="no"> - <source>Enter server manually</source> - <target state="translated">Увійдіть на сервер вручну</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error" xml:space="preserve" approved="no"> - <source>Error</source> - <target state="translated">Помилка</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accepting contact request" xml:space="preserve" approved="no"> - <source>Error accepting contact request</source> - <target state="translated">Помилка при прийнятті запиту на контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error accessing database file" xml:space="preserve" approved="no"> - <source>Error accessing database file</source> - <target state="translated">Помилка доступу до файлу бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error adding member(s)" xml:space="preserve" approved="no"> - <source>Error adding member(s)</source> - <target state="translated">Помилка додавання користувача(ів)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing address" xml:space="preserve" approved="no"> - <source>Error changing address</source> - <target state="translated">Помилка зміни адреси</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing role" xml:space="preserve" approved="no"> - <source>Error changing role</source> - <target state="translated">Помилка зміни ролі</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error changing setting" xml:space="preserve" approved="no"> - <source>Error changing setting</source> - <target state="translated">Помилка зміни налаштування</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating address" xml:space="preserve" approved="no"> - <source>Error creating address</source> - <target state="translated">Помилка створення адреси</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating group" xml:space="preserve" approved="no"> - <source>Error creating group</source> - <target state="translated">Помилка створення групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating group link" xml:space="preserve" approved="no"> - <source>Error creating group link</source> - <target state="translated">Помилка створення посилання на групу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error creating profile!" xml:space="preserve" approved="no"> - <source>Error creating profile!</source> - <target state="translated">Помилка створення профілю!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting chat database" xml:space="preserve" approved="no"> - <source>Error deleting chat database</source> - <target state="translated">Помилка видалення бази даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting chat!" xml:space="preserve" approved="no"> - <source>Error deleting chat!</source> - <target state="translated">Помилка видалення чату!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting connection" xml:space="preserve" approved="no"> - <source>Error deleting connection</source> - <target state="translated">Помилка видалення з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting contact" xml:space="preserve" approved="no"> - <source>Error deleting contact</source> - <target state="translated">Помилка видалення контакту</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting database" xml:space="preserve" approved="no"> - <source>Error deleting database</source> - <target state="translated">Помилка видалення бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting old database" xml:space="preserve" approved="no"> - <source>Error deleting old database</source> - <target state="translated">Помилка видалення старої бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting token" xml:space="preserve" approved="no"> - <source>Error deleting token</source> - <target state="translated">Помилка видалення токена</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error deleting user profile" xml:space="preserve" approved="no"> - <source>Error deleting user profile</source> - <target state="translated">Помилка видалення профілю користувача</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error enabling notifications" xml:space="preserve" approved="no"> - <source>Error enabling notifications</source> - <target state="translated">Помилка увімкнення сповіщень</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error encrypting database" xml:space="preserve" approved="no"> - <source>Error encrypting database</source> - <target state="translated">Помилка шифрування бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error exporting chat database" xml:space="preserve" approved="no"> - <source>Error exporting chat database</source> - <target state="translated">Помилка експорту бази даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error importing chat database" xml:space="preserve" approved="no"> - <source>Error importing chat database</source> - <target state="translated">Помилка імпорту бази даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error joining group" xml:space="preserve" approved="no"> - <source>Error joining group</source> - <target state="translated">Помилка приєднання до групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error receiving file" xml:space="preserve" approved="no"> - <source>Error receiving file</source> - <target state="translated">Помилка отримання файлу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error removing member" xml:space="preserve" approved="no"> - <source>Error removing member</source> - <target state="translated">Помилка видалення учасника</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving ICE servers" xml:space="preserve" approved="no"> - <source>Error saving ICE servers</source> - <target state="translated">Помилка збереження серверів ICE</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving SMP servers" xml:space="preserve"> - <source>Error saving SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving group profile" xml:space="preserve" approved="no"> - <source>Error saving group profile</source> - <target state="translated">Помилка збереження профілю групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error saving passphrase to keychain" xml:space="preserve" approved="no"> - <source>Error saving passphrase to keychain</source> - <target state="translated">Помилка збереження пароля на keychain</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error sending message" xml:space="preserve" approved="no"> - <source>Error sending message</source> - <target state="translated">Помилка надсилання повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error starting chat" xml:space="preserve" approved="no"> - <source>Error starting chat</source> - <target state="translated">Помилка запуску чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error stopping chat" xml:space="preserve" approved="no"> - <source>Error stopping chat</source> - <target state="translated">Помилка зупинки чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error switching profile!" xml:space="preserve" approved="no"> - <source>Error switching profile!</source> - <target state="translated">Помилка перемикання профілю!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating group link" xml:space="preserve" approved="no"> - <source>Error updating group link</source> - <target state="translated">Помилка оновлення посилання на групу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating message" xml:space="preserve" approved="no"> - <source>Error updating message</source> - <target state="translated">Повідомлення про помилку оновлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error updating settings" xml:space="preserve" approved="no"> - <source>Error updating settings</source> - <target state="translated">Помилка оновлення налаштувань</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: %@" xml:space="preserve" approved="no"> - <source>Error: %@</source> - <target state="translated">Помилка: %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: URL is invalid" xml:space="preserve" approved="no"> - <source>Error: URL is invalid</source> - <target state="translated">Помилка: URL-адреса невірна</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error: no database file" xml:space="preserve" approved="no"> - <source>Error: no database file</source> - <target state="translated">Помилка: немає файлу бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exit without saving" xml:space="preserve" approved="no"> - <source>Exit without saving</source> - <target state="translated">Вихід без збереження</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Export database" xml:space="preserve" approved="no"> - <source>Export database</source> - <target state="translated">Експорт бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Export error:" xml:space="preserve" approved="no"> - <source>Export error:</source> - <target state="translated">Помилка експорту:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exported database archive." xml:space="preserve" approved="no"> - <source>Exported database archive.</source> - <target state="translated">Експортований архів бази даних.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Exporting database archive..." xml:space="preserve" approved="no"> - <source>Exporting database archive...</source> - <target state="translated">Експорт архіву бази даних...</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Failed to remove passphrase" xml:space="preserve" approved="no"> - <source>Failed to remove passphrase</source> - <target state="translated">Не вдалося видалити парольну фразу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve" approved="no"> - <source>File will be received when your contact is online, please wait or check later!</source> - <target state="translated">Файл буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File: %@" xml:space="preserve" approved="no"> - <source>File: %@</source> - <target state="translated">Файл: %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Files & media" xml:space="preserve" approved="no"> - <source>Files & media</source> - <target state="translated">Файли та медіа</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="For console" xml:space="preserve" approved="no"> - <source>For console</source> - <target state="translated">Для консолі</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="French interface" xml:space="preserve" approved="no"> - <source>French interface</source> - <target state="translated">Французький інтерфейс</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full link" xml:space="preserve" approved="no"> - <source>Full link</source> - <target state="translated">Повне посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full name (optional)" xml:space="preserve" approved="no"> - <source>Full name (optional)</source> - <target state="translated">Повне ім'я (необов'язково)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Full name:" xml:space="preserve" approved="no"> - <source>Full name:</source> - <target state="translated">Повне ім'я:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="GIFs and stickers" xml:space="preserve" approved="no"> - <source>GIFs and stickers</source> - <target state="translated">GIF-файли та наклейки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group" xml:space="preserve" approved="no"> - <source>Group</source> - <target state="translated">Група</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group display name" xml:space="preserve" approved="no"> - <source>Group display name</source> - <target state="translated">Назва групи для відображення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group full name (optional)" xml:space="preserve" approved="no"> - <source>Group full name (optional)</source> - <target state="translated">Повна назва групи (необов'язково)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group image" xml:space="preserve" approved="no"> - <source>Group image</source> - <target state="translated">Зображення групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation" xml:space="preserve" approved="no"> - <source>Group invitation</source> - <target state="translated">Групове запрошення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation expired" xml:space="preserve" approved="no"> - <source>Group invitation expired</source> - <target state="translated">Термін дії групового запрошення закінчився</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve" approved="no"> - <source>Group invitation is no longer valid, it was removed by sender.</source> - <target state="translated">Групове запрошення більше не дійсне, воно було видалено відправником.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group link" xml:space="preserve" approved="no"> - <source>Group link</source> - <target state="translated">Посилання на групу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group links" xml:space="preserve" approved="no"> - <source>Group links</source> - <target state="translated">Групові посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve" approved="no"> - <source>Group members can irreversibly delete sent messages.</source> - <target state="translated">Учасники групи можуть безповоротно видаляти надіслані повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send direct messages." xml:space="preserve" approved="no"> - <source>Group members can send direct messages.</source> - <target state="translated">Учасники групи можуть надсилати прямі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send disappearing messages." xml:space="preserve" approved="no"> - <source>Group members can send disappearing messages.</source> - <target state="translated">Учасники групи можуть надсилати зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can send voice messages." xml:space="preserve" approved="no"> - <source>Group members can send voice messages.</source> - <target state="translated">Учасники групи можуть надсилати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group message:" xml:space="preserve" approved="no"> - <source>Group message:</source> - <target state="translated">Групове повідомлення:</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Group preferences" xml:space="preserve" approved="no"> - <source>Group preferences</source> - <target state="translated">Параметри груп</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group profile" xml:space="preserve" approved="no"> - <source>Group profile</source> - <target state="translated">Профіль групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve" approved="no"> - <source>Group profile is stored on members' devices, not on the servers.</source> - <target state="translated">Профіль групи зберігається на пристроях учасників, а не на серверах.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve" approved="no"> - <source>Group will be deleted for all members - this cannot be undone!</source> - <target state="translated">Група буде видалена для всіх учасників - це неможливо скасувати!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve" approved="no"> - <source>Group will be deleted for you - this cannot be undone!</source> - <target state="translated">Група буде видалена для вас - це не може бути скасовано!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Help" xml:space="preserve" approved="no"> - <source>Help</source> - <target state="translated">Довідка</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hidden" xml:space="preserve" approved="no"> - <source>Hidden</source> - <target state="translated">Приховано</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hide" xml:space="preserve" approved="no"> - <source>Hide</source> - <target state="translated">Приховати</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Hide app screen in the recent apps." xml:space="preserve" approved="no"> - <source>Hide app screen in the recent apps.</source> - <target state="translated">Приховати екран програми в останніх програмах.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How SimpleX works" xml:space="preserve" approved="no"> - <source>How SimpleX works</source> - <target state="translated">Як працює SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How it works" xml:space="preserve" approved="no"> - <source>How it works</source> - <target state="translated">Як це працює</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to" xml:space="preserve" approved="no"> - <source>How to</source> - <target state="translated">Як зробити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to use it" xml:space="preserve" approved="no"> - <source>How to use it</source> - <target state="translated">Як ним користуватися</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="How to use your servers" xml:space="preserve" approved="no"> - <source>How to use your servers</source> - <target state="translated">Як користуватися вашими серверами</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="ICE servers (one per line)" xml:space="preserve" approved="no"> - <source>ICE servers (one per line)</source> - <target state="translated">Сервери ICE (по одному на лінію)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you can't meet in person, **show QR code in the video call**, or share the link." xml:space="preserve"> - <source>If you can't meet in person, **show QR code in the video call**, or share the link.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve" approved="no"> - <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> - <target state="translated">Якщо ви не можете зустрітися особисто, ви можете **сканувати QR-код у відеодзвінку**, або ваш контакт може поділитися посиланням на запрошення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve" approved="no"> - <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> - <target state="translated">Якщо вам потрібно скористатися чатом зараз, натисніть **Зробити це пізніше** нижче (вам буде запропоновано перенести базу даних при перезапуску програми).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Ignore" xml:space="preserve" approved="no"> - <source>Ignore</source> - <target state="translated">Ігнорувати</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve" approved="no"> - <source>Image will be received when your contact is online, please wait or check later!</source> - <target state="translated">Зображення буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Immune to spam and abuse" xml:space="preserve" approved="no"> - <source>Immune to spam and abuse</source> - <target state="translated">Імунітет до спаму та зловживань</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import" xml:space="preserve" approved="no"> - <source>Import</source> - <target state="translated">Імпорт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import chat database?" xml:space="preserve" approved="no"> - <source>Import chat database?</source> - <target state="translated">Імпортувати базу даних чату?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Import database" xml:space="preserve" approved="no"> - <source>Import database</source> - <target state="translated">Імпорт бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Improved privacy and security" xml:space="preserve" approved="no"> - <source>Improved privacy and security</source> - <target state="translated">Покращена конфіденційність та безпека</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Improved server configuration" xml:space="preserve" approved="no"> - <source>Improved server configuration</source> - <target state="translated">Покращена конфігурація сервера</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito" xml:space="preserve" approved="no"> - <source>Incognito</source> - <target state="translated">Інкогніто</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode" xml:space="preserve" approved="no"> - <source>Incognito mode</source> - <target state="translated">Режим інкогніто</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve" approved="no"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target state="translated">Режим інкогніто тут не підтримується - ваш основний профіль буде надіслано учасникам групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve" approved="no"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target state="translated">Режим інкогніто захищає конфіденційність імені та зображення вашого основного профілю - для кожного нового контакту створюється новий випадковий профіль.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incoming audio call" xml:space="preserve" approved="no"> - <source>Incoming audio call</source> - <target state="translated">Вхідний аудіовиклик</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Incoming call" xml:space="preserve" approved="no"> - <source>Incoming call</source> - <target state="translated">Вхідний дзвінок</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Incoming video call" xml:space="preserve" approved="no"> - <source>Incoming video call</source> - <target state="translated">Вхідний відеодзвінок</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Incorrect security code!" xml:space="preserve" approved="no"> - <source>Incorrect security code!</source> - <target state="translated">Неправильний код безпеки!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve" approved="no"> - <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> - <target state="translated">Встановіть [SimpleX Chat для терміналу] (https://github.com/simplex-chat/simplex-chat)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve" approved="no"> - <source>Instant push notifications will be hidden! -</source> - <target state="translated">Миттєві пуш-сповіщення будуть приховані! -</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Instantly" xml:space="preserve" approved="no"> - <source>Instantly</source> - <target state="translated">Миттєво</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Interface" xml:space="preserve" approved="no"> - <source>Interface</source> - <target state="translated">Інтерфейс</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invalid connection link" xml:space="preserve" approved="no"> - <source>Invalid connection link</source> - <target state="translated">Неправильне посилання для підключення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invalid server address!" xml:space="preserve" approved="no"> - <source>Invalid server address!</source> - <target state="translated">Неправильна адреса сервера!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invitation expired!" xml:space="preserve" approved="no"> - <source>Invitation expired!</source> - <target state="translated">Термін дії запрошення закінчився!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite members" xml:space="preserve" approved="no"> - <source>Invite members</source> - <target state="translated">Запросити учасників</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Invite to group" xml:space="preserve" approved="no"> - <source>Invite to group</source> - <target state="translated">Запросити до групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion" xml:space="preserve" approved="no"> - <source>Irreversible message deletion</source> - <target state="translated">Безповоротне видалення повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve" approved="no"> - <source>Irreversible message deletion is prohibited in this chat.</source> - <target state="translated">У цьому чаті заборонено безповоротне видалення повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve" approved="no"> - <source>Irreversible message deletion is prohibited in this group.</source> - <target state="translated">У цій групі заборонено безповоротне видалення повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve" approved="no"> - <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> - <target state="translated">Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It can happen when: 1. The messages expire on the server if they were not received for 30 days, 2. The server you use to receive the messages from this contact was updated and restarted. 3. The connection is compromised. Please connect to the developers via Settings to receive the updates about the servers. We will be adding server redundancy to prevent lost messages." xml:space="preserve"> - <source>It can happen when: -1. The messages expire on the server if they were not received for 30 days, -2. The server you use to receive the messages from this contact was updated and restarted. -3. The connection is compromised. -Please connect to the developers via Settings to receive the updates about the servers. -We will be adding server redundancy to prevent lost messages.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve" approved="no"> - <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> - <target state="translated">Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Italian interface" xml:space="preserve" approved="no"> - <source>Italian interface</source> - <target state="translated">Італійський інтерфейс</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join" xml:space="preserve" approved="no"> - <source>Join</source> - <target state="translated">Приєднуйтесь</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join group" xml:space="preserve" approved="no"> - <source>Join group</source> - <target state="translated">Приєднуйтесь до групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Join incognito" xml:space="preserve" approved="no"> - <source>Join incognito</source> - <target state="translated">Приєднуйтесь інкогніто</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Joining group" xml:space="preserve" approved="no"> - <source>Joining group</source> - <target state="translated">Приєднання до групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Keychain error" xml:space="preserve" approved="no"> - <source>Keychain error</source> - <target state="translated">Помилка KeyChain</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="LIVE" xml:space="preserve" approved="no"> - <source>LIVE</source> - <target state="translated">НАЖИВО</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Large file!" xml:space="preserve" approved="no"> - <source>Large file!</source> - <target state="translated">Великий файл!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave" xml:space="preserve" approved="no"> - <source>Leave</source> - <target state="translated">Залишити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave group" xml:space="preserve" approved="no"> - <source>Leave group</source> - <target state="translated">Покинути групу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Leave group?" xml:space="preserve" approved="no"> - <source>Leave group?</source> - <target state="translated">Покинути групу?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Light" xml:space="preserve" approved="no"> - <source>Light</source> - <target state="translated">Світлий</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Limitations" xml:space="preserve" approved="no"> - <source>Limitations</source> - <target state="translated">Обмеження</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Live message!" xml:space="preserve" approved="no"> - <source>Live message!</source> - <target state="translated">Живе повідомлення!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Live messages" xml:space="preserve" approved="no"> - <source>Live messages</source> - <target state="translated">Живі повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Local name" xml:space="preserve" approved="no"> - <source>Local name</source> - <target state="translated">Місцева назва</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Local profile data only" xml:space="preserve" approved="no"> - <source>Local profile data only</source> - <target state="translated">Тільки локальні дані профілю</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make a private connection" xml:space="preserve" approved="no"> - <source>Make a private connection</source> - <target state="translated">Створіть приватне з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> - <source>Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve" approved="no"> - <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> - <target state="translated">Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve" approved="no"> - <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> - <target state="translated">Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark deleted for everyone" xml:space="preserve" approved="no"> - <source>Mark deleted for everyone</source> - <target state="translated">Позначити видалено для всіх</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark read" xml:space="preserve" approved="no"> - <source>Mark read</source> - <target state="translated">Позначити прочитано</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mark verified" xml:space="preserve" approved="no"> - <source>Mark verified</source> - <target state="translated">Позначити перевірено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Markdown in messages" xml:space="preserve" approved="no"> - <source>Markdown in messages</source> - <target state="translated">Виправлення в повідомленнях</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve" approved="no"> - <source>Max 30 seconds, received instantly.</source> - <target state="translated">Максимум 30 секунд, отримується миттєво.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member" xml:space="preserve" approved="no"> - <source>Member</source> - <target state="translated">Учасник</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve" approved="no"> - <source>Member role will be changed to "%@". All group members will be notified.</source> - <target state="translated">Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve" approved="no"> - <source>Member role will be changed to "%@". The member will receive a new invitation.</source> - <target state="translated">Роль учасника буде змінено на "%@". Учасник отримає нове запрошення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve" approved="no"> - <source>Member will be removed from group - this cannot be undone!</source> - <target state="translated">Учасник буде видалений з групи - це неможливо скасувати!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message delivery error" xml:space="preserve" approved="no"> - <source>Message delivery error</source> - <target state="translated">Помилка доставки повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message draft" xml:space="preserve" approved="no"> - <source>Message draft</source> - <target state="translated">Чернетка повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Message text" xml:space="preserve" approved="no"> - <source>Message text</source> - <target state="translated">Текст повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Messages" xml:space="preserve" approved="no"> - <source>Messages</source> - <target state="translated">Повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migrating database archive..." xml:space="preserve" approved="no"> - <source>Migrating database archive...</source> - <target state="translated">Перенесення архіву бази даних...</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration error:" xml:space="preserve" approved="no"> - <source>Migration error:</source> - <target state="translated">Помилка міграції:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve" approved="no"> - <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> - <target state="translated">Міграція не вдалася. Натисніть **Пропустити** нижче, щоб продовжити використовувати поточну базу даних. Будь ласка, повідомте про проблему розробникам програми через чат або електронну пошту [chat@simplex.chat](mailto:chat@simplex.chat).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migration is completed" xml:space="preserve" approved="no"> - <source>Migration is completed</source> - <target state="translated">Міграцію завершено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Moderate" xml:space="preserve" approved="no"> - <source>Moderate</source> - <target state="translated">Модерується</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="More improvements are coming soon!" xml:space="preserve" approved="no"> - <source>More improvements are coming soon!</source> - <target state="translated">Незабаром буде ще більше покращень!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve" approved="no"> - <source>Most likely this contact has deleted the connection with you.</source> - <target state="translated">Швидше за все, цей контакт видалив зв'язок з вами.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Multiple chat profiles" xml:space="preserve" approved="no"> - <source>Multiple chat profiles</source> - <target state="translated">Кілька профілів чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Mute" xml:space="preserve" approved="no"> - <source>Mute</source> - <target state="translated">Вимкнути звук</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Name" xml:space="preserve" approved="no"> - <source>Name</source> - <target state="translated">Ім'я</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network & servers" xml:space="preserve" approved="no"> - <source>Network & servers</source> - <target state="translated">Мережа та сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network settings" xml:space="preserve" approved="no"> - <source>Network settings</source> - <target state="translated">Налаштування мережі</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Network status" xml:space="preserve" approved="no"> - <source>Network status</source> - <target state="translated">Стан мережі</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New contact request" xml:space="preserve" approved="no"> - <source>New contact request</source> - <target state="translated">Новий запит на контакт</target> - <note>notification</note> - </trans-unit> - <trans-unit id="New contact:" xml:space="preserve" approved="no"> - <source>New contact:</source> - <target state="translated">Новий контакт:</target> - <note>notification</note> - </trans-unit> - <trans-unit id="New database archive" xml:space="preserve" approved="no"> - <source>New database archive</source> - <target state="translated">Новий архів бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New in %@" xml:space="preserve" approved="no"> - <source>New in %@</source> - <target state="translated">Нове в %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New member role" xml:space="preserve" approved="no"> - <source>New member role</source> - <target state="translated">Нова роль учасника</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="New message" xml:space="preserve" approved="no"> - <source>New message</source> - <target state="translated">Нове повідомлення</target> - <note>notification</note> - </trans-unit> - <trans-unit id="New passphrase…" xml:space="preserve" approved="no"> - <source>New passphrase…</source> - <target state="translated">Новий пароль…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No" xml:space="preserve" approved="no"> - <source>No</source> - <target state="translated">Ні</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No contacts selected" xml:space="preserve" approved="no"> - <source>No contacts selected</source> - <target state="translated">Не вибрано жодного контакту</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No contacts to add" xml:space="preserve" approved="no"> - <source>No contacts to add</source> - <target state="translated">Немає контактів для додавання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No device token!" xml:space="preserve" approved="no"> - <source>No device token!</source> - <target state="translated">Токен пристрою відсутній!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No group!" xml:space="preserve" approved="no"> - <source>Group not found!</source> - <target state="translated">Групу не знайдено!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No permission to record voice message" xml:space="preserve" approved="no"> - <source>No permission to record voice message</source> - <target state="translated">Немає дозволу на запис голосового повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="No received or sent files" xml:space="preserve" approved="no"> - <source>No received or sent files</source> - <target state="translated">Немає отриманих або відправлених файлів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Notifications" xml:space="preserve" approved="no"> - <source>Notifications</source> - <target state="translated">Сповіщення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Notifications are disabled!" xml:space="preserve" approved="no"> - <source>Notifications are disabled!</source> - <target state="translated">Сповіщення вимкнено!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Off (Local)" xml:space="preserve" approved="no"> - <source>Off (Local)</source> - <target state="translated">Вимкнено (локально)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Ok" xml:space="preserve" approved="no"> - <source>Ok</source> - <target state="translated">Гаразд</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database" xml:space="preserve" approved="no"> - <source>Old database</source> - <target state="translated">Стара база даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Old database archive" xml:space="preserve" approved="no"> - <source>Old database archive</source> - <target state="translated">Старий архів бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="One-time invitation link" xml:space="preserve" approved="no"> - <source>One-time invitation link</source> - <target state="translated">Посилання на одноразове запрошення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve" approved="no"> - <source>Onion hosts will be required for connection. Requires enabling VPN.</source> - <target state="translated">Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve" approved="no"> - <source>Onion hosts will be used when available. Requires enabling VPN.</source> - <target state="translated">Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Onion hosts will not be used." xml:space="preserve" approved="no"> - <source>Onion hosts will not be used.</source> - <target state="translated">Onion хости не будуть використовуватися.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve" approved="no"> - <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> - <target state="translated">Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can change group preferences." xml:space="preserve" approved="no"> - <source>Only group owners can change group preferences.</source> - <target state="translated">Тільки власники груп можуть змінювати налаштування групи.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only group owners can enable voice messages." xml:space="preserve" approved="no"> - <source>Only group owners can enable voice messages.</source> - <target state="translated">Тільки власники груп можуть вмикати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve" approved="no"> - <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> - <target state="translated">Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can send disappearing messages." xml:space="preserve" approved="no"> - <source>Only you can send disappearing messages.</source> - <target state="translated">Тільки ви можете надсилати зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only you can send voice messages." xml:space="preserve" approved="no"> - <source>Only you can send voice messages.</source> - <target state="translated">Тільки ви можете надсилати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve" approved="no"> - <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> - <target state="translated">Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve" approved="no"> - <source>Only your contact can send disappearing messages.</source> - <target state="translated">Тільки ваш контакт може надсилати зникаючі повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Only your contact can send voice messages." xml:space="preserve" approved="no"> - <source>Only your contact can send voice messages.</source> - <target state="translated">Тільки ваш контакт може надсилати голосові повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open Settings" xml:space="preserve" approved="no"> - <source>Open Settings</source> - <target state="translated">Відкрийте Налаштування</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open chat" xml:space="preserve" approved="no"> - <source>Open chat</source> - <target state="translated">Відкритий чат</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Open chat console" xml:space="preserve" approved="no"> - <source>Open chat console</source> - <target state="translated">Відкрийте консоль чату</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Open user profiles" xml:space="preserve" approved="no"> - <source>Open user profiles</source> - <target state="translated">Відкрити профілі користувачів</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve" approved="no"> - <source>Open-source protocol and code – anybody can run the servers.</source> - <target state="translated">Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve" approved="no"> - <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> - <target state="translated">Відкриття посилання в браузері може знизити конфіденційність і безпеку з'єднання. Ненадійні посилання SimpleX будуть червоного кольору.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="PING count" xml:space="preserve" approved="no"> - <source>PING count</source> - <target state="translated">Кількість PING</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="PING interval" xml:space="preserve" approved="no"> - <source>PING interval</source> - <target state="translated">Інтервал PING</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste" xml:space="preserve" approved="no"> - <source>Paste</source> - <target state="translated">Вставити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste image" xml:space="preserve" approved="no"> - <source>Paste image</source> - <target state="translated">Вставити зображення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste received link" xml:space="preserve" approved="no"> - <source>Paste received link</source> - <target state="translated">Вставте отримане посилання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve" approved="no"> - <source>Paste the link you received into the box below to connect with your contact.</source> - <target state="translated">Вставте отримане посилання у поле нижче, щоб зв'язатися з вашим контактом.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="People can connect to you only via the links you share." xml:space="preserve" approved="no"> - <source>People can connect to you only via the links you share.</source> - <target state="translated">Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Periodically" xml:space="preserve" approved="no"> - <source>Periodically</source> - <target state="translated">Періодично</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve" approved="no"> - <source>Please ask your contact to enable sending voice messages.</source> - <target state="translated">Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve" approved="no"> - <source>Please check that you used the correct link or ask your contact to send you another one.</source> - <target state="translated">Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve" approved="no"> - <source>Please check your network connection with %@ and try again.</source> - <target state="translated">Будь ласка, перевірте підключення до мережі за допомогою %@ і спробуйте ще раз.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please check yours and your contact preferences." xml:space="preserve" approved="no"> - <source>Please check yours and your contact preferences.</source> - <target state="translated">Будь ласка, перевірте свої та контактні налаштування.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please contact group admin." xml:space="preserve" approved="no"> - <source>Please contact group admin.</source> - <target state="translated">Зверніться до адміністратора групи.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please enter correct current passphrase." xml:space="preserve" approved="no"> - <source>Please enter correct current passphrase.</source> - <target state="translated">Будь ласка, введіть правильний поточний пароль.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve" approved="no"> - <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> - <target state="translated">Будь ласка, введіть попередній пароль після відновлення резервної копії бази даних. Ця дія не може бути скасована.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve" approved="no"> - <source>Please restart the app and migrate the database to enable push notifications.</source> - <target state="translated">Будь ласка, перезапустіть додаток і перенесіть базу даних, щоб увімкнути push-сповіщення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve" approved="no"> - <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> - <target state="translated">Будь ласка, зберігайте пароль надійно, ви НЕ зможете отримати доступ до чату, якщо втратите його.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve" approved="no"> - <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> - <target state="translated">Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve" approved="no"> - <source>Possibly, certificate fingerprint in server address is incorrect</source> - <target state="translated">Можливо, в адресі сервера неправильно вказано відбиток сертифіката</target> - <note>server test error</note> - </trans-unit> - <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve" approved="no"> - <source>Preserve the last message draft, with attachments.</source> - <target state="translated">Зберегти чернетку останнього повідомлення з вкладеннями.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preset server" xml:space="preserve" approved="no"> - <source>Preset server</source> - <target state="translated">Попередньо встановлений сервер</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preset server address" xml:space="preserve" approved="no"> - <source>Preset server address</source> - <target state="translated">Попередньо встановлена адреса сервера</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Privacy & security" xml:space="preserve" approved="no"> - <source>Privacy & security</source> - <target state="translated">Конфіденційність і безпека</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Privacy redefined" xml:space="preserve" approved="no"> - <source>Privacy redefined</source> - <target state="translated">Конфіденційність переглянута</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Private filenames" xml:space="preserve" approved="no"> - <source>Private filenames</source> - <target state="translated">Приватні імена файлів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile and server connections" xml:space="preserve" approved="no"> - <source>Profile and server connections</source> - <target state="translated">З'єднання профілю та сервера</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile image" xml:space="preserve" approved="no"> - <source>Profile image</source> - <target state="translated">Зображення профілю</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve" approved="no"> - <source>Prohibit irreversible message deletion.</source> - <target state="translated">Заборонити незворотне видалення повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve" approved="no"> - <source>Prohibit sending direct messages to members.</source> - <target state="translated">Заборонити надсилати прямі повідомлення учасникам.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve" approved="no"> - <source>Prohibit sending disappearing messages.</source> - <target state="translated">Заборонити надсилання зникаючих повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit sending voice messages." xml:space="preserve" approved="no"> - <source>Prohibit sending voice messages.</source> - <target state="translated">Заборонити надсилання голосових повідомлень.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protect app screen" xml:space="preserve" approved="no"> - <source>Protect app screen</source> - <target state="translated">Захистіть екран програми</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Protocol timeout" xml:space="preserve" approved="no"> - <source>Protocol timeout</source> - <target state="translated">Тайм-аут протоколу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Push notifications" xml:space="preserve" approved="no"> - <source>Push notifications</source> - <target state="translated">Push-повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Rate the app" xml:space="preserve" approved="no"> - <source>Rate the app</source> - <target state="translated">Оцініть додаток</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read" xml:space="preserve" approved="no"> - <source>Read</source> - <target state="translated">Читати</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our GitHub repository." xml:space="preserve" approved="no"> - <source>Read more in our GitHub repository.</source> - <target state="translated">Читайте більше в нашому репозиторії на GitHub.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve" approved="no"> - <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> - <target state="translated">Читайте більше в нашому [GitHub репозиторії] (https://github.com/simplex-chat/simplex-chat#readme).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Received file event" xml:space="preserve" approved="no"> - <source>Received file event</source> - <target state="translated">Подія отримання файлу</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Receiving via" xml:space="preserve" approved="no"> - <source>Receiving via</source> - <target state="translated">Отримання через</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Recipients see updates as you type them." xml:space="preserve" approved="no"> - <source>Recipients see updates as you type them.</source> - <target state="translated">Одержувачі бачать оновлення, коли ви їх вводите.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reduced battery usage" xml:space="preserve" approved="no"> - <source>Reduced battery usage</source> - <target state="translated">Зменшення використання акумулятора</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reject" xml:space="preserve" approved="no"> - <source>Reject</source> - <target state="translated">Відхилити</target> - <note>reject incoming call via notification</note> - </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve" approved="no"> - <source>Reject contact (sender NOT notified)</source> - <target state="translated">Відхилити контакт (відправника НЕ повідомлено)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reject contact request" xml:space="preserve" approved="no"> - <source>Reject contact request</source> - <target state="translated">Відхилити запит на контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve" approved="no"> - <source>Relay server is only used if necessary. Another party can observe your IP address.</source> - <target state="translated">Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve" approved="no"> - <source>Relay server protects your IP address, but it can observe the duration of the call.</source> - <target state="translated">Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove" xml:space="preserve" approved="no"> - <source>Remove</source> - <target state="translated">Видалити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove member" xml:space="preserve" approved="no"> - <source>Remove member</source> - <target state="translated">Видалити учасника</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove member?" xml:space="preserve" approved="no"> - <source>Remove member?</source> - <target state="translated">Видалити учасника?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Remove passphrase from keychain?" xml:space="preserve" approved="no"> - <source>Remove passphrase from keychain?</source> - <target state="translated">Видалити парольну фразу з брелока?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reply" xml:space="preserve" approved="no"> - <source>Reply</source> - <target state="translated">Відповісти</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Required" xml:space="preserve" approved="no"> - <source>Required</source> - <target state="translated">Потрібно</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset" xml:space="preserve" approved="no"> - <source>Reset</source> - <target state="translated">Перезавантаження</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset colors" xml:space="preserve" approved="no"> - <source>Reset colors</source> - <target state="translated">Скинути кольори</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reset to defaults" xml:space="preserve" approved="no"> - <source>Reset to defaults</source> - <target state="translated">Відновити налаштування за замовчуванням</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve" approved="no"> - <source>Restart the app to create a new chat profile</source> - <target state="translated">Перезапустіть програму, щоб створити новий профіль чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restart the app to use imported chat database" xml:space="preserve" approved="no"> - <source>Restart the app to use imported chat database</source> - <target state="translated">Перезапустіть програму, щоб використовувати імпортовану базу даних чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore" xml:space="preserve" approved="no"> - <source>Restore</source> - <target state="translated">Відновити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database backup" xml:space="preserve" approved="no"> - <source>Restore database backup</source> - <target state="translated">Відновлення резервної копії бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database backup?" xml:space="preserve" approved="no"> - <source>Restore database backup?</source> - <target state="translated">Відновити резервну копію бази даних?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Restore database error" xml:space="preserve" approved="no"> - <source>Restore database error</source> - <target state="translated">Відновлення помилки бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Reveal" xml:space="preserve" approved="no"> - <source>Reveal</source> - <target state="translated">Показувати</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Revert" xml:space="preserve" approved="no"> - <source>Revert</source> - <target state="translated">Повернутися</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Role" xml:space="preserve" approved="no"> - <source>Role</source> - <target state="translated">Роль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Run chat" xml:space="preserve" approved="no"> - <source>Run chat</source> - <target state="translated">Запустити чат</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SMP servers" xml:space="preserve" approved="no"> - <source>SMP servers</source> - <target state="translated">Сервери SMP</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save" xml:space="preserve" approved="no"> - <source>Save</source> - <target state="translated">Зберегти</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Save (and notify contacts)" xml:space="preserve" approved="no"> - <source>Save (and notify contacts)</source> - <target state="translated">Зберегти (і повідомити контактам)</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and notify contact" xml:space="preserve" approved="no"> - <source>Save and notify contact</source> - <target state="translated">Зберегти та повідомити контакт</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save and notify group members" xml:space="preserve" approved="no"> - <source>Save and notify group members</source> - <target state="translated">Зберегти та повідомити учасників групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save archive" xml:space="preserve" approved="no"> - <source>Save archive</source> - <target state="translated">Зберегти архів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save group profile" xml:space="preserve" approved="no"> - <source>Save group profile</source> - <target state="translated">Зберегти профіль групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save passphrase and open chat" xml:space="preserve" approved="no"> - <source>Save passphrase and open chat</source> - <target state="translated">Збережіть пароль і відкрийте чат</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save passphrase in Keychain" xml:space="preserve" approved="no"> - <source>Save passphrase in Keychain</source> - <target state="translated">Збережіть парольну фразу в Keychain</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save preferences?" xml:space="preserve" approved="no"> - <source>Save preferences?</source> - <target state="translated">Зберегти налаштування?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save servers" xml:space="preserve" approved="no"> - <source>Save servers</source> - <target state="translated">Зберегти сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve" approved="no"> - <source>Saved WebRTC ICE servers will be removed</source> - <target state="translated">Збережені сервери WebRTC ICE буде видалено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan QR code" xml:space="preserve" approved="no"> - <source>Scan QR code</source> - <target state="translated">Відскануйте QR-код</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan code" xml:space="preserve" approved="no"> - <source>Scan code</source> - <target state="translated">Сканувати код</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan security code from your contact's app." xml:space="preserve" approved="no"> - <source>Scan security code from your contact's app.</source> - <target state="translated">Відскануйте код безпеки з додатку вашого контакту.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Scan server QR code" xml:space="preserve" approved="no"> - <source>Scan server QR code</source> - <target state="translated">Відскануйте QR-код сервера</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Search" xml:space="preserve" approved="no"> - <source>Search</source> - <target state="translated">Пошук</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Secure queue" xml:space="preserve" approved="no"> - <source>Secure queue</source> - <target state="translated">Безпечна черга</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Security assessment" xml:space="preserve" approved="no"> - <source>Security assessment</source> - <target state="translated">Оцінка безпеки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Security code" xml:space="preserve" approved="no"> - <source>Security code</source> - <target state="translated">Код безпеки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send" xml:space="preserve" approved="no"> - <source>Send</source> - <target state="translated">Надіслати</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve" approved="no"> - <source>Send a live message - it will update for the recipient(s) as you type it</source> - <target state="translated">Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send direct message" xml:space="preserve" approved="no"> - <source>Send direct message</source> - <target state="translated">Надішліть пряме повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send link previews" xml:space="preserve" approved="no"> - <source>Send link previews</source> - <target state="translated">Надіслати попередній перегляд за посиланням</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send live message" xml:space="preserve" approved="no"> - <source>Send live message</source> - <target state="translated">Надіслати живе повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send notifications" xml:space="preserve" approved="no"> - <source>Send notifications</source> - <target state="translated">Надсилати сповіщення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send notifications:" xml:space="preserve" approved="no"> - <source>Send notifications:</source> - <target state="translated">Надсилати сповіщення:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send questions and ideas" xml:space="preserve" approved="no"> - <source>Send questions and ideas</source> - <target state="translated">Надсилайте запитання та ідеї</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve" approved="no"> - <source>Send them from gallery or custom keyboards.</source> - <target state="translated">Надсилайте їх із галереї чи власних клавіатур.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sender cancelled file transfer." xml:space="preserve" approved="no"> - <source>Sender cancelled file transfer.</source> - <target state="translated">Відправник скасував передачу файлу.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sender may have deleted the connection request." xml:space="preserve" approved="no"> - <source>Sender may have deleted the connection request.</source> - <target state="translated">Можливо, відправник видалив запит на підключення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sending via" xml:space="preserve" approved="no"> - <source>Sending via</source> - <target state="translated">Надсилання через</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Sent file event" xml:space="preserve" approved="no"> - <source>Sent file event</source> - <target state="translated">Подія надісланого файлу</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve" approved="no"> - <source>Sent messages will be deleted after set time.</source> - <target state="translated">Надіслані повідомлення будуть видалені через встановлений час.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve" approved="no"> - <source>Server requires authorization to create queues, check password</source> - <target state="translated">Сервер вимагає авторизації для створення черг, перевірте пароль</target> - <note>server test error</note> - </trans-unit> - <trans-unit id="Server test failed!" xml:space="preserve" approved="no"> - <source>Server test failed!</source> - <target state="translated">Тест сервера завершився невдало!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Servers" xml:space="preserve" approved="no"> - <source>Servers</source> - <target state="translated">Сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set 1 day" xml:space="preserve" approved="no"> - <source>Set 1 day</source> - <target state="translated">Встановити 1 день</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set contact name…" xml:space="preserve" approved="no"> - <source>Set contact name…</source> - <target state="translated">Встановити ім'я контакту…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set group preferences" xml:space="preserve" approved="no"> - <source>Set group preferences</source> - <target state="translated">Встановіть налаштування групи</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set passphrase to export" xml:space="preserve" approved="no"> - <source>Set passphrase to export</source> - <target state="translated">Встановити ключову фразу для експорту</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve" approved="no"> - <source>Set timeouts for proxy/VPN</source> - <target state="translated">Встановлення таймаутів для проксі/VPN</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Settings" xml:space="preserve" approved="no"> - <source>Settings</source> - <target state="translated">Налаштування</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share" xml:space="preserve" approved="no"> - <source>Share</source> - <target state="translated">Поділіться</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Share invitation link" xml:space="preserve"> - <source>Share invitation link</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share link" xml:space="preserve" approved="no"> - <source>Share link</source> - <target state="translated">Поділіться посиланням</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Share one-time invitation link" xml:space="preserve" approved="no"> - <source>Share one-time invitation link</source> - <target state="translated">Поділіться посиланням на одноразове запрошення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show QR code" xml:space="preserve"> - <source>Show QR code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show calls in phone history" xml:space="preserve" approved="no"> - <source>Show calls in phone history</source> - <target state="translated">Показувати дзвінки в історії дзвінків</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Show preview" xml:space="preserve" approved="no"> - <source>Show preview</source> - <target state="translated">Показати попередній перегляд</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve" approved="no"> - <source>SimpleX Chat security was audited by Trail of Bits.</source> - <target state="translated">Безпека SimpleX Chat була перевірена компанією Trail of Bits.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock" xml:space="preserve" approved="no"> - <source>SimpleX Lock</source> - <target state="translated">SimpleX Lock</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX Lock turned on" xml:space="preserve" approved="no"> - <source>SimpleX Lock turned on</source> - <target state="translated">SimpleX Lock увімкнено</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX contact address" xml:space="preserve" approved="no"> - <source>SimpleX contact address</source> - <target state="translated">Контактна адреса SimpleX</target> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve" approved="no"> - <source>SimpleX encrypted message or connection event</source> - <target state="translated">Зашифроване повідомлення SimpleX або подія підключення</target> - <note>notification</note> - </trans-unit> - <trans-unit id="SimpleX group link" xml:space="preserve" approved="no"> - <source>SimpleX group link</source> - <target state="translated">Посилання на групу SimpleX</target> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="SimpleX links" xml:space="preserve" approved="no"> - <source>SimpleX links</source> - <target state="translated">Посилання SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="SimpleX one-time invitation" xml:space="preserve" approved="no"> - <source>SimpleX one-time invitation</source> - <target state="translated">Одноразове запрошення SimpleX</target> - <note>simplex link type</note> - </trans-unit> - <trans-unit id="Skip" xml:space="preserve" approved="no"> - <source>Skip</source> - <target state="translated">Пропустити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Skipped messages" xml:space="preserve" approved="no"> - <source>Skipped messages</source> - <target state="translated">Пропущені повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Somebody" xml:space="preserve" approved="no"> - <source>Somebody</source> - <target state="translated">Хтось</target> - <note>notification title</note> - </trans-unit> - <trans-unit id="Start a new chat" xml:space="preserve" approved="no"> - <source>Start a new chat</source> - <target state="translated">Почніть новий чат</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Start chat" xml:space="preserve" approved="no"> - <source>Start chat</source> - <target state="translated">Почати чат</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Start migration" xml:space="preserve" approved="no"> - <source>Start migration</source> - <target state="translated">Почати міграцію</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop" xml:space="preserve" approved="no"> - <source>Stop</source> - <target state="translated">Зупинити</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop SimpleX" xml:space="preserve" approved="no"> - <source>Stop SimpleX</source> - <target state="translated">Зупинити SimpleX</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Stop chat to enable database actions" xml:space="preserve" approved="no"> - <source>Stop chat to enable database actions</source> - <target state="translated">Зупиніть чат, щоб увімкнути дії з базою даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve" approved="no"> - <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> - <target state="translated">Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Stop chat?" xml:space="preserve" approved="no"> - <source>Stop chat?</source> - <target state="translated">Зупинити чат?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Support SimpleX Chat" xml:space="preserve" approved="no"> - <source>Support SimpleX Chat</source> - <target state="translated">Підтримка чату SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="System" xml:space="preserve" approved="no"> - <source>System</source> - <target state="translated">Система</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP connection timeout" xml:space="preserve" approved="no"> - <source>TCP connection timeout</source> - <target state="translated">Тайм-аут TCP-з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPCNT" xml:space="preserve" approved="no"> - <source>TCP_KEEPCNT</source> - <target state="translated">TCP_KEEPCNT</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPIDLE" xml:space="preserve" approved="no"> - <source>TCP_KEEPIDLE</source> - <target state="translated">TCP_KEEPIDLE</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="TCP_KEEPINTVL" xml:space="preserve" approved="no"> - <source>TCP_KEEPINTVL</source> - <target state="translated">TCP_KEEPINTVL</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Take picture" xml:space="preserve" approved="no"> - <source>Take picture</source> - <target state="translated">Сфотографуйте</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap button " xml:space="preserve" approved="no"> - <source>Tap button </source> - <target state="translated">Натисніть кнопку </target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to join" xml:space="preserve" approved="no"> - <source>Tap to join</source> - <target state="translated">Натисніть, щоб приєднатися</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to join incognito" xml:space="preserve" approved="no"> - <source>Tap to join incognito</source> - <target state="translated">Натисніть, щоб приєднатися інкогніто</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tap to start a new chat" xml:space="preserve" approved="no"> - <source>Tap to start a new chat</source> - <target state="translated">Натисніть, щоб почати новий чат</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Test failed at step %@." xml:space="preserve" approved="no"> - <source>Test failed at step %@.</source> - <target state="translated">Тест завершився невдало на кроці %@.</target> - <note>server test failure</note> - </trans-unit> - <trans-unit id="Test server" xml:space="preserve" approved="no"> - <source>Test server</source> - <target state="translated">Тестовий сервер</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Test servers" xml:space="preserve" approved="no"> - <source>Test servers</source> - <target state="translated">Тестові сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Tests failed!" xml:space="preserve" approved="no"> - <source>Tests failed!</source> - <target state="translated">Тести не пройшли!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve" approved="no"> - <source>Thank you for installing SimpleX Chat!</source> - <target state="translated">Дякуємо, що встановили SimpleX Chat!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve" approved="no"> - <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> - <target state="translated">Дякуємо користувачам - [внесок через Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve" approved="no"> - <source>Thanks to the users – contribute via Weblate!</source> - <target state="translated">Дякуємо користувачам - зробіть свій внесок через Weblate!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve" approved="no"> - <source>The 1st platform without any user identifiers – private by design.</source> - <target state="translated">Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve" approved="no"> - <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> - <target state="translated">Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve" approved="no"> - <source>The attempt to change database passphrase was not completed.</source> - <target state="translated">Спроба змінити пароль до бази даних не була завершена.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve" approved="no"> - <source>The connection you accepted will be cancelled!</source> - <target state="translated">Прийняте вами з'єднання буде скасовано!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve" approved="no"> - <source>The contact you shared this link with will NOT be able to connect!</source> - <target state="translated">Контакт, з яким ви поділилися цим посиланням, НЕ зможе підключитися!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve" approved="no"> - <source>The created archive is available via app Settings / Database / Old database archive.</source> - <target state="translated">Створений архів доступний через Налаштування програми / База даних / Старий архів бази даних.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve" approved="no"> - <source>The group is fully decentralized – it is visible only to the members.</source> - <target state="translated">Група повністю децентралізована - її бачать лише учасники.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The message will be deleted for all members." xml:space="preserve" approved="no"> - <source>The message will be deleted for all members.</source> - <target state="translated">Повідомлення буде видалено для всіх учасників.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve" approved="no"> - <source>The message will be marked as moderated for all members.</source> - <target state="translated">Повідомлення буде позначено як модероване для всіх учасників.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The next generation of private messaging" xml:space="preserve" approved="no"> - <source>The next generation of private messaging</source> - <target state="translated">Наступне покоління приватних повідомлень</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve" approved="no"> - <source>The old database was not removed during the migration, it can be deleted.</source> - <target state="translated">Стара база даних не була видалена під час міграції, її можна видалити.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The profile is only shared with your contacts." xml:space="preserve" approved="no"> - <source>The profile is only shared with your contacts.</source> - <target state="translated">Профіль доступний лише вашим контактам.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The sender will NOT be notified" xml:space="preserve" approved="no"> - <source>The sender will NOT be notified</source> - <target state="translated">Відправник НЕ буде повідомлений</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve" approved="no"> - <source>The servers for new connections of your current chat profile **%@**.</source> - <target state="translated">Сервери для нових підключень вашого поточного профілю чату **%@**.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Theme" xml:space="preserve" approved="no"> - <source>Theme</source> - <target state="translated">Тема</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve" approved="no"> - <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> - <target state="translated">Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve" approved="no"> - <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> - <target state="translated">Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve" approved="no"> - <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> - <target state="translated">Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member)." xml:space="preserve"> - <source>This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This group no longer exists." xml:space="preserve" approved="no"> - <source>This group no longer exists.</source> - <target state="translated">Цієї групи більше не існує.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve" approved="no"> - <source>This setting applies to messages in your current chat profile **%@**.</source> - <target state="translated">Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve" approved="no"> - <source>To ask any questions and to receive updates:</source> - <target state="translated">Задати будь-які питання та отримувати новини:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve" approved="no"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target state="translated">Щоб знайти профіль, який використовується для з'єднання інкогніто, торкніться імені контакту або групи у верхній частині чату.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To make a new connection" xml:space="preserve" approved="no"> - <source>To make a new connection</source> - <target state="translated">Щоб створити нове з'єднання</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve" approved="no"> - <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> - <target state="translated">Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve" approved="no"> - <source>To protect timezone, image/voice files use UTC.</source> - <target state="translated">Для захисту часового поясу у файлах зображень/голосу використовується UTC.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve" approved="no"> - <source>To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled.</source> - <target state="translated">Щоб захистити вашу інформацію, увімкніть SimpleX Lock. -Перед увімкненням цієї функції вам буде запропоновано пройти автентифікацію.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve" approved="no"> - <source>To record voice message please grant permission to use Microphone.</source> - <target state="translated">Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve" approved="no"> - <source>To support instant push notifications the chat database has to be migrated.</source> - <target state="translated">Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve" approved="no"> - <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> - <target state="translated">Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Transport isolation" xml:space="preserve" approved="no"> - <source>Transport isolation</source> - <target state="translated">Транспортна ізоляція</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve" approved="no"> - <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> - <target state="translated">Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve" approved="no"> - <source>Trying to connect to the server used to receive messages from this contact.</source> - <target state="translated">Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off" xml:space="preserve" approved="no"> - <source>Turn off</source> - <target state="translated">Вимкнути</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn off notifications?" xml:space="preserve" approved="no"> - <source>Turn off notifications?</source> - <target state="translated">Вимкнути сповіщення?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Turn on" xml:space="preserve" approved="no"> - <source>Turn on</source> - <target state="translated">Ввімкнути</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unable to record voice message" xml:space="preserve" approved="no"> - <source>Unable to record voice message</source> - <target state="translated">Не вдається записати голосове повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unexpected error: %@" xml:space="preserve" approved="no"> - <source>Unexpected error: %@</source> - <target state="translated">Неочікувана помилка: %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unexpected migration state" xml:space="preserve" approved="no"> - <source>Unexpected migration state</source> - <target state="translated">Неочікуваний стан міграції</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unknown caller" xml:space="preserve" approved="no"> - <source>Unknown caller</source> - <target state="translated">Невідомий абонент</target> - <note>callkit banner</note> - </trans-unit> - <trans-unit id="Unknown database error: %@" xml:space="preserve" approved="no"> - <source>Unknown database error: %@</source> - <target state="translated">Невідома помилка бази даних: %@</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unknown error" xml:space="preserve" approved="no"> - <source>Unknown error</source> - <target state="translated">Невідома помилка</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve" approved="no"> - <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> - <target state="translated">Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим "Не турбувати", щоб уникнути переривань.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve" approved="no"> - <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> - <target state="translated">Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це. -Щоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unlock" xml:space="preserve" approved="no"> - <source>Unlock</source> - <target state="translated">Розблокувати</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Unmute" xml:space="preserve" approved="no"> - <source>Unmute</source> - <target state="translated">Увімкнути звук</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Unread" xml:space="preserve" approved="no"> - <source>Unread</source> - <target state="translated">Непрочитане</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update" xml:space="preserve" approved="no"> - <source>Update</source> - <target state="translated">Оновлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update .onion hosts setting?" xml:space="preserve" approved="no"> - <source>Update .onion hosts setting?</source> - <target state="translated">Оновити налаштування хостів .onion?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update database passphrase" xml:space="preserve" approved="no"> - <source>Update database passphrase</source> - <target state="translated">Оновити парольну фразу бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update network settings?" xml:space="preserve" approved="no"> - <source>Update network settings?</source> - <target state="translated">Оновити налаштування мережі?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Update transport isolation mode?" xml:space="preserve" approved="no"> - <source>Update transport isolation mode?</source> - <target state="translated">Оновити режим транспортної ізоляції?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> - <source>Updating settings will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> - <source>Updating this setting will re-connect the client to all servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use .onion hosts" xml:space="preserve"> - <source>Use .onion hosts</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> - <source>Use SimpleX Chat servers?</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use chat" xml:space="preserve"> - <source>Use chat</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use for new connections" xml:space="preserve"> - <source>Use for new connections</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use iOS call interface" xml:space="preserve"> - <source>Use iOS call interface</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Use server" xml:space="preserve"> - <source>Use server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="User profile" xml:space="preserve"> - <source>User profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> - <source>Using .onion hosts requires compatible VPN provider.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> - <source>Using SimpleX Chat servers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Verify connection security" xml:space="preserve"> - <source>Verify connection security</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Verify security code" xml:space="preserve"> - <source>Verify security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Via browser" xml:space="preserve"> - <source>Via browser</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Video call" xml:space="preserve"> - <source>Video call</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="View security code" xml:space="preserve"> - <source>View security code</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages" xml:space="preserve"> - <source>Voice messages</source> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> - <source>Voice messages are prohibited in this chat.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> - <source>Voice messages are prohibited in this group.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice messages prohibited!" xml:space="preserve"> - <source>Voice messages prohibited!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Voice message…" xml:space="preserve"> - <source>Voice message…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for file" xml:space="preserve"> - <source>Waiting for file</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Waiting for image" xml:space="preserve"> - <source>Waiting for image</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="WebRTC ICE servers" xml:space="preserve"> - <source>WebRTC ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Welcome %@!" xml:space="preserve"> - <source>Welcome %@!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Welcome message" xml:space="preserve"> - <source>Welcome message</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="What's new" xml:space="preserve"> - <source>What's new</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When available" xml:space="preserve"> - <source>When available</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> - <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="With optional welcome message." xml:space="preserve"> - <source>With optional welcome message.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Wrong database passphrase" xml:space="preserve"> - <source>Wrong database passphrase</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Wrong passphrase!" xml:space="preserve"> - <source>Wrong passphrase!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You" xml:space="preserve"> - <source>You</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You accepted connection" xml:space="preserve"> - <source>You accepted connection</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You allow" xml:space="preserve"> - <source>You allow</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> - <source>You already have a chat profile with the same display name. Please choose another name.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are already connected to %@." xml:space="preserve"> - <source>You are already connected to %@.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> - <source>You are connected to the server used to receive messages from this contact.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You are invited to group" xml:space="preserve"> - <source>You are invited to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> - <source>You can accept calls from lock screen, without device and app authentication.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> - <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can now send messages to %@" xml:space="preserve"> - <source>You can now send messages to %@</source> - <note>notification body</note> - </trans-unit> - <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> - <source>You can set lock screen notification preview via settings.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> - <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it." xml:space="preserve"> - <source>You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> - <source>You can start chat via app Settings / Database or by restarting the app</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> - <source>You can use markdown to format messages:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You can't send messages!" xml:space="preserve"> - <source>You can't send messages!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> - <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You could not be verified; please try again." xml:space="preserve"> - <source>You could not be verified; please try again.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have no chats" xml:space="preserve"> - <source>You have no chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> - <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You joined this group" xml:space="preserve"> - <source>You joined this group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> - <source>You joined this group. Connecting to inviting group member.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> - <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> - <source>You need to allow your contact to send voice messages to be able to send them.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You rejected group invitation" xml:space="preserve"> - <source>You rejected group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You sent group invitation" xml:space="preserve"> - <source>You sent group invitation</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> - <source>You will be connected to group when the group host's device is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> - <source>You will be connected when your connection request is accepted, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> - <source>You will be connected when your contact's device is online, please wait or check later!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> - <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> - <source>You will join a group this link refers to and connect to its group members.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> - <source>You will stop receiving messages from this group. Chat history will be preserved.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> - <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> - <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your ICE servers" xml:space="preserve"> - <source>Your ICE servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SMP servers" xml:space="preserve"> - <source>Your SMP servers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your SimpleX contact address" xml:space="preserve"> - <source>Your SimpleX contact address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your calls" xml:space="preserve"> - <source>Your calls</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat database" xml:space="preserve"> - <source>Your chat database</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> - <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> - <source>Your chat profile will be sent to group members</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profiles" xml:space="preserve"> - <source>Your chat profiles</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chat profiles are stored locally, only on your device." xml:space="preserve"> - <source>Your chat profiles are stored locally, only on your device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your chats" xml:space="preserve"> - <source>Your chats</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact address" xml:space="preserve"> - <source>Your contact address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact can scan it from the app." xml:space="preserve"> - <source>Your contact can scan it from the app.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> - <source>Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> - <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> - <source>Your contacts can allow full message deletion.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> - <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your current profile" xml:space="preserve"> - <source>Your current profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your preferences" xml:space="preserve"> - <source>Your preferences</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your privacy" xml:space="preserve"> - <source>Your privacy</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> - <source>Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> - <source>Your profile, contacts and delivered messages are stored on your device.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your random profile" xml:space="preserve"> - <source>Your random profile</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server" xml:space="preserve"> - <source>Your server</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your server address" xml:space="preserve"> - <source>Your server address</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Your settings" xml:space="preserve"> - <source>Your settings</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> - <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> - <source>[Send us email](mailto:chat@simplex.chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> - <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="_italic_" xml:space="preserve"> - <source>\_italic_</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="`a + b`" xml:space="preserve"> - <source>\`a + b`</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="above, then choose:" xml:space="preserve"> - <source>above, then choose:</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="accepted call" xml:space="preserve"> - <source>accepted call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="admin" xml:space="preserve"> - <source>admin</source> - <note>member role</note> - </trans-unit> - <trans-unit id="always" xml:space="preserve"> - <source>always</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> - <source>audio call (not e2e encrypted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="bad message ID" xml:space="preserve"> - <source>bad message ID</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="bad message hash" xml:space="preserve"> - <source>bad message hash</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="bold" xml:space="preserve"> - <source>bold</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="call error" xml:space="preserve"> - <source>call error</source> - <note>call status</note> - </trans-unit> - <trans-unit id="call in progress" xml:space="preserve"> - <source>call in progress</source> - <note>call status</note> - </trans-unit> - <trans-unit id="calling…" xml:space="preserve"> - <source>calling…</source> - <note>call status</note> - </trans-unit> - <trans-unit id="cancelled %@" xml:space="preserve"> - <source>cancelled %@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="changed address for you" xml:space="preserve"> - <source>changed address for you</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="changed role of %@ to %@" xml:space="preserve"> - <source>changed role of %1$@ to %2$@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="changed your role to %@" xml:space="preserve"> - <source>changed your role to %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="changing address for %@..." xml:space="preserve"> - <source>changing address for %@...</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="changing address..." xml:space="preserve"> - <source>changing address...</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="colored" xml:space="preserve"> - <source>colored</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="complete" xml:space="preserve"> - <source>complete</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> - <source>connect to SimpleX Chat developers.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connected" xml:space="preserve"> - <source>connected</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting" xml:space="preserve"> - <source>connecting</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (accepted)" xml:space="preserve"> - <source>connecting (accepted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (announced)" xml:space="preserve"> - <source>connecting (announced)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (introduced)" xml:space="preserve"> - <source>connecting (introduced)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> - <source>connecting (introduction invitation)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="connecting call" xml:space="preserve"> - <source>connecting call…</source> - <note>call status</note> - </trans-unit> - <trans-unit id="connecting…" xml:space="preserve"> - <source>connecting…</source> - <note>chat list item title</note> - </trans-unit> - <trans-unit id="connection established" xml:space="preserve"> - <source>connection established</source> - <note>chat list item title (it should not be shown</note> - </trans-unit> - <trans-unit id="connection:%@" xml:space="preserve"> - <source>connection:%@</source> - <note>connection information</note> - </trans-unit> - <trans-unit id="contact has e2e encryption" xml:space="preserve"> - <source>contact has e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="contact has no e2e encryption" xml:space="preserve"> - <source>contact has no e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="creator" xml:space="preserve"> - <source>creator</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="default (%@)" xml:space="preserve"> - <source>default (%@)</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="deleted" xml:space="preserve"> - <source>deleted</source> - <note>deleted chat item</note> - </trans-unit> - <trans-unit id="deleted group" xml:space="preserve"> - <source>deleted group</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="direct" xml:space="preserve"> - <source>direct</source> - <note>connection level description</note> - </trans-unit> - <trans-unit id="duplicate message" xml:space="preserve"> - <source>duplicate message</source> - <note>integrity error chat item</note> - </trans-unit> - <trans-unit id="e2e encrypted" xml:space="preserve"> - <source>e2e encrypted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="enabled" xml:space="preserve"> - <source>enabled</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="enabled for contact" xml:space="preserve"> - <source>enabled for contact</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="enabled for you" xml:space="preserve"> - <source>enabled for you</source> - <note>enabled status</note> - </trans-unit> - <trans-unit id="ended" xml:space="preserve"> - <source>ended</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="ended call %@" xml:space="preserve"> - <source>ended call %@</source> - <note>call status</note> - </trans-unit> - <trans-unit id="error" xml:space="preserve"> - <source>error</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="group deleted" xml:space="preserve"> - <source>group deleted</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="group profile updated" xml:space="preserve"> - <source>group profile updated</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> - <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> - <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="incognito via contact address link" xml:space="preserve"> - <source>incognito via contact address link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="incognito via group link" xml:space="preserve"> - <source>incognito via group link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="incognito via one-time link" xml:space="preserve"> - <source>incognito via one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="indirect (%d)" xml:space="preserve"> - <source>indirect (%d)</source> - <note>connection level description</note> - </trans-unit> - <trans-unit id="invalid chat" xml:space="preserve"> - <source>invalid chat</source> - <note>invalid chat data</note> - </trans-unit> - <trans-unit id="invalid chat data" xml:space="preserve"> - <source>invalid chat data</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="invalid data" xml:space="preserve"> - <source>invalid data</source> - <note>invalid chat item</note> - </trans-unit> - <trans-unit id="invitation to group %@" xml:space="preserve"> - <source>invitation to group %@</source> - <note>group name</note> - </trans-unit> - <trans-unit id="invited" xml:space="preserve"> - <source>invited</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="invited %@" xml:space="preserve"> - <source>invited %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="invited to connect" xml:space="preserve"> - <source>invited to connect</source> - <note>chat list item title</note> - </trans-unit> - <trans-unit id="invited via your group link" xml:space="preserve"> - <source>invited via your group link</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="italic" xml:space="preserve"> - <source>italic</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="join as %@" xml:space="preserve"> - <source>join as %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="left" xml:space="preserve"> - <source>left</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="marked deleted" xml:space="preserve"> - <source>marked deleted</source> - <note>marked deleted chat item preview text</note> - </trans-unit> - <trans-unit id="member" xml:space="preserve"> - <source>member</source> - <note>member role</note> - </trans-unit> - <trans-unit id="member connected" xml:space="preserve"> - <source>connected</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="message received" xml:space="preserve"> - <source>message received</source> - <note>notification</note> - </trans-unit> - <trans-unit id="missed call" xml:space="preserve"> - <source>missed call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="moderated" xml:space="preserve"> - <source>moderated</source> - <note>moderated chat item</note> - </trans-unit> - <trans-unit id="moderated by %@" xml:space="preserve"> - <source>moderated by %@</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="never" xml:space="preserve"> - <source>never</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="new message" xml:space="preserve"> - <source>new message</source> - <note>notification</note> - </trans-unit> - <trans-unit id="no" xml:space="preserve"> - <source>no</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="no e2e encryption" xml:space="preserve"> - <source>no e2e encryption</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="observer" xml:space="preserve"> - <source>observer</source> - <note>member role</note> - </trans-unit> - <trans-unit id="off" xml:space="preserve"> - <source>off</source> - <note>enabled status - group pref value</note> - </trans-unit> - <trans-unit id="offered %@" xml:space="preserve"> - <source>offered %@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="offered %@: %@" xml:space="preserve"> - <source>offered %1$@: %2$@</source> - <note>feature offered item</note> - </trans-unit> - <trans-unit id="on" xml:space="preserve"> - <source>on</source> - <note>group pref value</note> - </trans-unit> - <trans-unit id="or chat with the developers" xml:space="preserve"> - <source>or chat with the developers</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="owner" xml:space="preserve"> - <source>owner</source> - <note>member role</note> - </trans-unit> - <trans-unit id="peer-to-peer" xml:space="preserve"> - <source>peer-to-peer</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="received answer…" xml:space="preserve"> - <source>received answer…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="received confirmation…" xml:space="preserve"> - <source>received confirmation…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="rejected call" xml:space="preserve"> - <source>rejected call</source> - <note>call status</note> - </trans-unit> - <trans-unit id="removed" xml:space="preserve"> - <source>removed</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="removed %@" xml:space="preserve"> - <source>removed %@</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="removed you" xml:space="preserve"> - <source>removed you</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="sec" xml:space="preserve"> - <source>sec</source> - <note>network option</note> - </trans-unit> - <trans-unit id="secret" xml:space="preserve"> - <source>secret</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="starting…" xml:space="preserve"> - <source>starting…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="strike" xml:space="preserve"> - <source>strike</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="this contact" xml:space="preserve"> - <source>this contact</source> - <note>notification title</note> - </trans-unit> - <trans-unit id="unknown" xml:space="preserve"> - <source>unknown</source> - <note>connection info</note> - </trans-unit> - <trans-unit id="updated group profile" xml:space="preserve"> - <source>updated group profile</source> - <note>rcv group event chat item</note> - </trans-unit> - <trans-unit id="v%@ (%@)" xml:space="preserve"> - <source>v%@ (%@)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="via contact address link" xml:space="preserve"> - <source>via contact address link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via group link" xml:space="preserve"> - <source>via group link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via one-time link" xml:space="preserve"> - <source>via one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="via relay" xml:space="preserve"> - <source>via relay</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> - <source>video call (not e2e encrypted)</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="waiting for answer…" xml:space="preserve"> - <source>waiting for answer…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="waiting for confirmation…" xml:space="preserve"> - <source>waiting for confirmation…</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="wants to connect to you!" xml:space="preserve"> - <source>wants to connect to you!</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="yes" xml:space="preserve"> - <source>yes</source> - <note>pref value</note> - </trans-unit> - <trans-unit id="you are invited to group" xml:space="preserve"> - <source>you are invited to group</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="you are observer" xml:space="preserve"> - <source>you are observer</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="you changed address" xml:space="preserve"> - <source>you changed address</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="you changed address for %@" xml:space="preserve"> - <source>you changed address for %@</source> - <note>chat item text</note> - </trans-unit> - <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> - <source>you changed role for yourself to %@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> - <source>you changed role of %1$@ to %2$@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you left" xml:space="preserve"> - <source>you left</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you removed %@" xml:space="preserve"> - <source>you removed %@</source> - <note>snd group event chat item</note> - </trans-unit> - <trans-unit id="you shared one-time link" xml:space="preserve"> - <source>you shared one-time link</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="you shared one-time link incognito" xml:space="preserve"> - <source>you shared one-time link incognito</source> - <note>chat list item description</note> - </trans-unit> - <trans-unit id="you: " xml:space="preserve"> - <source>you: </source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="~strike~" xml:space="preserve"> - <source>\~strike~</source> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%@ servers" xml:space="preserve" approved="no"> - <source>%@ servers</source> - <target state="translated">%@ сервери</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="%lld seconds" xml:space="preserve" approved="no"> - <source>%lld seconds</source> - <target state="translated">%lld секунд</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Audio and video calls" xml:space="preserve" approved="no"> - <source>Audio and video calls</source> - <target state="translated">Аудіо та відеодзвінки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Authentication cancelled" xml:space="preserve" approved="no"> - <source>Authentication cancelled</source> - <target state="translated">Аутентифікацію скасовано</target> - <note>PIN entry</note> - </trans-unit> - <trans-unit id="Can't delete user profile!" xml:space="preserve" approved="no"> - <source>Can't delete user profile!</source> - <target state="translated">Не можу видалити профіль користувача!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change lock mode" xml:space="preserve" approved="no"> - <source>Change lock mode</source> - <target state="translated">Зміна режиму блокування</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Create file" xml:space="preserve" approved="no"> - <source>Create file</source> - <target state="translated">Створити файл</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Database upgrade" xml:space="preserve" approved="no"> - <source>Database upgrade</source> - <target state="translated">Оновлення бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete chat profile" xml:space="preserve" approved="no"> - <source>Delete chat profile</source> - <target state="translated">Видалити профіль чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Delete file" xml:space="preserve" approved="no"> - <source>Delete file</source> - <target state="translated">Видалити файл</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Change passcode" xml:space="preserve" approved="no"> - <source>Change passcode</source> - <target state="translated">Змінити пароль</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="Allow message reactions." xml:space="preserve" approved="no"> - <source>Allow message reactions.</source> - <target state="translated">Дозволити реакцію на повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve" approved="no"> - <source>App passcode is replaced with self-destruct passcode.</source> - <target state="translated">Пароль програми замінено на пароль самознищення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve" approved="no"> - <source>Both you and your contact can add message reactions.</source> - <target state="translated">Реакції на повідомлення можете додавати як ви, так і ваш контакт.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change self-destruct passcode" xml:space="preserve" approved="no"> - <source>Change self-destruct passcode</source> - <target state="translated">Змінити пароль самознищення</target> - <note>authentication reason - set passcode view</note> - </trans-unit> - <trans-unit id="Chinese and Spanish interface" xml:space="preserve" approved="no"> - <source>Chinese and Spanish interface</source> - <target state="translated">Інтерфейс китайською та іспанською мовами</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Compare file" xml:space="preserve" approved="no"> - <source>Compare file</source> - <target state="translated">Порівняти файл</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Confirm Passcode" xml:space="preserve" approved="no"> - <source>Confirm Passcode</source> - <target state="translated">Підтвердити пароль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm password" xml:space="preserve" approved="no"> - <source>Confirm password</source> - <target state="translated">Підтвердити пароль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Confirm database upgrades" xml:space="preserve" approved="no"> - <source>Confirm database upgrades</source> - <target state="translated">Підтвердити оновлення бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database downgrade" xml:space="preserve" approved="no"> - <source>Database downgrade</source> - <target state="translated">Пониження версії бази даних</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Current Passcode" xml:space="preserve" approved="no"> - <source>Current Passcode</source> - <target state="translated">Поточний пароль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve" approved="no"> - <source>Database IDs and Transport isolation option.</source> - <target state="translated">Ідентифікатори бази даних та опція ізоляції транспорту.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="5 minutes" xml:space="preserve" approved="no"> - <source>5 minutes</source> - <target state="translated">5 хвилин</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="30 seconds" xml:space="preserve" approved="no"> - <source>30 seconds</source> - <target state="translated">30 секунд</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve" approved="no"> - <source>Allow your contacts adding message reactions.</source> - <target state="translated">Дозвольте вашим контактам додавати реакції на повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Change self-destruct mode" xml:space="preserve" approved="no"> - <source>Change self-destruct mode</source> - <target state="translated">Змінити режим самознищення</target> - <note>authentication reason</note> - </trans-unit> - <trans-unit id="%@ (current)" xml:space="preserve" approved="no"> + <trans-unit id="%@ (current)" xml:space="preserve"> <source>%@ (current)</source> - <target state="translated">%@ (поточний)</target> + <target>%@ (поточний)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%@ (current):" xml:space="preserve" approved="no"> + <trans-unit id="%@ (current):" xml:space="preserve"> <source>%@ (current):</source> - <target state="translated">%@ (поточний):</target> + <target>%@ (поточний):</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%@:" xml:space="preserve" approved="no"> + <trans-unit id="%@ / %@" xml:space="preserve"> + <source>%@ / %@</source> + <target>%@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ і %@ підключено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ at %@:" xml:space="preserve"> + <source>%1$@ at %2$@:</source> + <target>%1$@ за %2$@:</target> + <note>copied message info, <sender> at <time></note> + </trans-unit> + <trans-unit id="%@ is connected!" xml:space="preserve"> + <source>%@ is connected!</source> + <target>%@ підключено!</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="%@ is not verified" xml:space="preserve"> + <source>%@ is not verified</source> + <target>%@ не перевірено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ is verified" xml:space="preserve"> + <source>%@ is verified</source> + <target>%@ перевірено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ servers" xml:space="preserve"> + <source>%@ servers</source> + <target>%@ сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@ wants to connect!" xml:space="preserve"> + <source>%@ wants to connect!</source> + <target>%@ хоче підключитися!</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ та %lld інші підключені учасники</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> - <target state="translated">%@:</target> + <target>%@:</target> <note>copied message info</note> </trans-unit> - <trans-unit id="%d weeks" xml:space="preserve" approved="no"> - <source>%d weeks</source> - <target state="translated">%d тижнів</target> + <trans-unit id="%d days" xml:space="preserve"> + <source>%d days</source> + <target>%d днів</target> <note>time interval</note> </trans-unit> - <trans-unit id="%u messages skipped." xml:space="preserve" approved="no"> + <trans-unit id="%d hours" xml:space="preserve"> + <source>%d hours</source> + <target>%d годин</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d min" xml:space="preserve"> + <source>%d min</source> + <target>%d хв</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d months" xml:space="preserve"> + <source>%d months</source> + <target>%d місяців</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d sec" xml:space="preserve"> + <source>%d sec</source> + <target>%d сек</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%d skipped message(s)" xml:space="preserve"> + <source>%d skipped message(s)</source> + <target>%d пропущено повідомлення(ь)</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="%d weeks" xml:space="preserve"> + <source>%d weeks</source> + <target>%d тижнів</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="%lld" xml:space="preserve"> + <source>%lld</source> + <target>%lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld %@" xml:space="preserve"> + <source>%lld %@</source> + <target>%lld %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld contact(s) selected" xml:space="preserve"> + <source>%lld contact(s) selected</source> + <target>%lld контакт(и) вибрані</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld file(s) with total size of %@" xml:space="preserve"> + <source>%lld file(s) with total size of %@</source> + <target>%lld файл(и) загальним розміром %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld members" xml:space="preserve"> + <source>%lld members</source> + <target>%lld учасників</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld minutes" xml:space="preserve"> + <source>%lld minutes</source> + <target>%lld хвилин</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld second(s)" xml:space="preserve"> + <source>%lld second(s)</source> + <target>%lld секунд(и)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lld seconds" xml:space="preserve"> + <source>%lld seconds</source> + <target>%lld секунд</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldd" xml:space="preserve"> + <source>%lldd</source> + <target>%lldd</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldh" xml:space="preserve"> + <source>%lldh</source> + <target>%lldh</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldk" xml:space="preserve"> + <source>%lldk</source> + <target>%lldk</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldm" xml:space="preserve"> + <source>%lldm</source> + <target>%lldm</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldmth" xml:space="preserve"> + <source>%lldmth</source> + <target>%lldmth</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%llds" xml:space="preserve"> + <source>%llds</source> + <target>%llds</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%lldw" xml:space="preserve"> + <source>%lldw</source> + <target>%lldw</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%u messages failed to decrypt." xml:space="preserve"> + <source>%u messages failed to decrypt.</source> + <target>%u повідомлень не вдалося розшифрувати.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="%u messages skipped." xml:space="preserve"> <source>%u messages skipped.</source> - <target state="translated">%u повідомлень пропущено.</target> + <target>%u повідомлень пропущено.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="0s" xml:space="preserve" approved="no"> - <source>0s</source> - <target state="translated">0с</target> + <trans-unit id="(" xml:space="preserve"> + <source>(</source> + <target>(</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="1 minute" xml:space="preserve" approved="no"> - <source>1 minute</source> - <target state="translated">1 хвилина</target> + <trans-unit id=")" xml:space="preserve"> + <source>)</source> + <target>)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve" approved="no"> - <source>Allow message reactions only if your contact allows them.</source> - <target state="translated">Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх.</target> + <trans-unit id="**Add new contact**: to create your one-time QR Code for your contact." xml:space="preserve"> + <source>**Add new contact**: to create your one-time QR Code or link for your contact.</source> + <target>**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve" approved="no"> - <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> - <target state="translated">Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі.</target> + <trans-unit id="**Create link / QR code** for your contact to use." xml:space="preserve"> + <source>**Create link / QR code** for your contact to use.</source> + <target>**Створіть посилання / QR-код** для використання вашим контактом.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All your contacts will remain connected." xml:space="preserve" approved="no"> - <source>All your contacts will remain connected.</source> - <target state="translated">Всі ваші контакти залишаться на зв'язку.</target> + <trans-unit id="**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." xml:space="preserve"> + <source>**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have.</source> + <target>**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Custom time" xml:space="preserve" approved="no"> - <source>Custom time</source> - <target state="translated">Індивідуальний час</target> + <trans-unit id="**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." xml:space="preserve"> + <source>**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app).</source> + <target>**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Database ID: %d" xml:space="preserve" approved="no"> - <source>Database ID: %d</source> - <target state="translated">Ідентифікатор бази даних: %d</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="1-time link" xml:space="preserve" approved="no"> - <source>1-time link</source> - <target state="translated">1-разове посилання</target> + <trans-unit id="**Paste received link** or open it in the browser and tap **Open in mobile app**." xml:space="preserve"> + <source>**Paste received link** or open it in the browser and tap **Open in mobile app**.</source> + <target>**Вставте отримане посилання** або відкрийте його в браузері і натисніть **Відкрити в мобільному додатку**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Address" xml:space="preserve" approved="no"> - <source>Address</source> - <target state="translated">Адреса</target> + <trans-unit id="**Please note**: you will NOT be able to recover or change passphrase if you lose it." xml:space="preserve"> + <source>**Please note**: you will NOT be able to recover or change passphrase if you lose it.</source> + <target>**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="About SimpleX address" xml:space="preserve" approved="no"> - <source>About SimpleX address</source> - <target state="translated">Про адресу SimpleX</target> + <trans-unit id="**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." xml:space="preserve"> + <source>**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from.</source> + <target>**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve" approved="no"> - <source>Allow calls only if your contact allows them.</source> - <target state="translated">Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх.</target> + <trans-unit id="**Scan QR code**: to connect to your contact in person or via video call." xml:space="preserve"> + <source>**Scan QR code**: to connect to your contact in person or via video call.</source> + <target>**Відскануйте QR-код**: щоб з'єднатися з вашим контактом особисто або за допомогою відеодзвінка.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Allow your contacts to call you." xml:space="preserve" approved="no"> - <source>Allow your contacts to call you.</source> - <target state="translated">Дозвольте вашим контактам телефонувати вам.</target> + <trans-unit id="**Warning**: Instant push notifications require passphrase saved in Keychain." xml:space="preserve"> + <source>**Warning**: Instant push notifications require passphrase saved in Keychain.</source> + <target>**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="App passcode" xml:space="preserve" approved="no"> - <source>App passcode</source> - <target state="translated">Пароль додатку</target> + <trans-unit id="**e2e encrypted** audio call" xml:space="preserve"> + <source>**e2e encrypted** audio call</source> + <target>**e2e encrypted** аудіодзвінок</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio/video calls" xml:space="preserve" approved="no"> - <source>Audio/video calls</source> - <target state="translated">Аудіо/відео дзвінки</target> - <note>chat feature</note> - </trans-unit> - <trans-unit id="Auto-accept" xml:space="preserve" approved="no"> - <source>Auto-accept</source> - <target state="translated">Автоприйняття</target> + <trans-unit id="**e2e encrypted** video call" xml:space="preserve"> + <source>**e2e encrypted** video call</source> + <target>**e2e encrypted** відеодзвінок</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Audio/video calls are prohibited." xml:space="preserve" approved="no"> - <source>Audio/video calls are prohibited.</source> - <target state="translated">Аудіо/відео дзвінки заборонені.</target> + <trans-unit id="*bold*" xml:space="preserve"> + <source>\*bold*</source> + <target>\*жирний*</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Bad message ID" xml:space="preserve" approved="no"> - <source>Bad message ID</source> - <target state="translated">Неправильний ідентифікатор повідомлення</target> + <trans-unit id=", " xml:space="preserve"> + <source>, </source> + <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Bad message hash" xml:space="preserve" approved="no"> - <source>Bad message hash</source> - <target state="translated">Поганий хеш повідомлення</target> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Both you and your contact can make calls." xml:space="preserve" approved="no"> - <source>Both you and your contact can make calls.</source> - <target state="translated">Дзвонити можете як ви, так і ваш контакт.</target> + <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> + <source>- more stable message delivery. +- a bit better groups. +- and more!</source> + <target>- стабільніша доставка повідомлень. +- трохи кращі групи. +- і багато іншого!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Create SimpleX address" xml:space="preserve" approved="no"> - <source>Create SimpleX address</source> - <target state="translated">Створіть адресу SimpleX</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Continue" xml:space="preserve" approved="no"> - <source>Continue</source> - <target state="translated">Продовжуйте</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Create an address to let people connect with you." xml:space="preserve" approved="no"> - <source>Create an address to let people connect with you.</source> - <target state="translated">Створіть адресу, щоб люди могли з вами зв'язатися.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Decryption error" xml:space="preserve" approved="no"> - <source>Decryption error</source> - <target state="translated">Помилка розшифровки</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve" approved="no"> + <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> <source>- voice messages up to 5 minutes. - custom time to disappear. - editing history.</source> - <target state="translated">- голосові повідомлення до 5 хвилин. + <target>- голосові повідомлення до 5 хвилин. - користувальницький час зникнення. - історія редагування.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All data is erased when it is entered." xml:space="preserve" approved="no"> - <source>All data is erased when it is entered.</source> - <target state="translated">Всі дані стираються при введенні.</target> + <trans-unit id="." xml:space="preserve"> + <source>.</source> + <target>.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Better messages" xml:space="preserve" approved="no"> - <source>Better messages</source> - <target state="translated">Кращі повідомлення</target> + <trans-unit id="0s" xml:space="preserve"> + <source>0s</source> + <target>0с</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%u messages failed to decrypt." xml:space="preserve" approved="no"> - <source>%u messages failed to decrypt.</source> - <target state="translated">%u повідомлень не вдалося розшифрувати.</target> + <trans-unit id="1 day" xml:space="preserve"> + <source>1 day</source> + <target>1 день</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="1 hour" xml:space="preserve"> + <source>1 hour</source> + <target>1 година</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="1 minute" xml:space="preserve"> + <source>1 minute</source> + <target>1 хвилина</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="%lld minutes" xml:space="preserve" approved="no"> - <source>%lld minutes</source> - <target state="translated">%lld хвилин</target> + <trans-unit id="1 month" xml:space="preserve"> + <source>1 month</source> + <target>1 місяць</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="1 week" xml:space="preserve"> + <source>1 week</source> + <target>1 тиждень</target> + <note>time interval</note> + </trans-unit> + <trans-unit id="1-time link" xml:space="preserve"> + <source>1-time link</source> + <target>1-разове посилання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve" approved="no"> + <trans-unit id="5 minutes" xml:space="preserve"> + <source>5 minutes</source> + <target>5 хвилин</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="6" xml:space="preserve"> + <source>6</source> + <target>6</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="30 seconds" xml:space="preserve"> + <source>30 seconds</source> + <target>30 секунд</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id=": " xml:space="preserve"> + <source>: </source> + <target>: </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="<p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>" xml:space="preserve"> <source><p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p></source> - <target state="translated"><p>Привіт!</p> + <target><p>Привіт!</p> <p><a href="%@"> Зв'яжіться зі мною через SimpleX Chat</a></p></target> <note>email text</note> </trans-unit> - <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> - <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> - <target state="translated">Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target> + <trans-unit id="A few more things" xml:space="preserve"> + <source>A few more things</source> + <target>Ще кілька речей</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Add welcome message" xml:space="preserve" approved="no"> - <source>Add welcome message</source> - <target state="translated">Додати вітальне повідомлення</target> + <trans-unit id="A new contact" xml:space="preserve"> + <source>A new contact</source> + <target>Новий контакт</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>Буде створено новий випадковий профіль.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All app data is deleted." xml:space="preserve" approved="no"> - <source>All app data is deleted.</source> - <target state="translated">Всі дані програми видаляються.</target> + <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> + <source>A separate TCP connection will be used **for each chat profile you have in the app**.</source> + <target>Для кожного профілю чату, який ви маєте в додатку, буде використовуватися окреме TCP-з'єднання.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve" approved="no"> - <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> - <target state="translated">Всі ваші контакти залишаться на зв'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target> + <trans-unit id="A separate TCP connection will be used **for each contact and group member**. **Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." xml:space="preserve"> + <source>A separate TCP connection will be used **for each contact and group member**. +**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.</source> + <target>Для кожного контакту та учасника групи буде використовуватися окреме TCP-з'єднання. +**Зверніть увагу: якщо у вас багато з'єднань, споживання заряду акумулятора і трафіку може бути значно вищим, а деякі з'єднання можуть обірватися.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Delete profile" xml:space="preserve" approved="no"> - <source>Delete profile</source> - <target state="translated">Видалити профіль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enable lock" xml:space="preserve" approved="no"> - <source>Enable lock</source> - <target state="translated">Увімкнути блокування</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Enter Passcode" xml:space="preserve" approved="no"> - <source>Enter Passcode</source> - <target state="translated">Введіть пароль</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Error aborting address change" xml:space="preserve" approved="no"> - <source>Error aborting address change</source> - <target state="translated">Помилка скасування зміни адреси</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Favorite" xml:space="preserve" approved="no"> - <source>Favorite</source> - <target state="translated">Улюблений</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve" approved="no"> - <source>File will be received when your contact completes uploading it.</source> - <target state="translated">Файл буде отримано, коли ваш контакт завершить завантаження.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Further reduced battery usage" xml:space="preserve" approved="no"> - <source>Further reduced battery usage</source> - <target state="translated">Подальше зменшення використання акумулятора</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Group members can add message reactions." xml:space="preserve" approved="no"> - <source>Group members can add message reactions.</source> - <target state="translated">Учасники групи можуть додавати реакції на повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Hidden chat profiles" xml:space="preserve" approved="no"> - <source>Hidden chat profiles</source> - <target state="translated">Приховані профілі чату</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve" approved="no"> - <source>If you can't meet in person, show QR code in a video call, or share the link.</source> - <target state="translated">Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve" approved="no"> - <source>If you enter your self-destruct passcode while opening the app:</source> - <target state="translated">Якщо ви введете пароль самознищення під час відкриття програми:</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Info" xml:space="preserve" approved="no"> - <source>Info</source> - <target state="translated">Інформація</target> - <note>chat item action</note> - </trans-unit> - <trans-unit id="Invite friends" xml:space="preserve" approved="no"> - <source>Invite friends</source> - <target state="translated">Запросити друзів</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Finally, we have them! 🚀" xml:space="preserve" approved="no"> - <source>Finally, we have them! 🚀</source> - <target state="translated">Нарешті, вони у нас є! 🚀</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="History" xml:space="preserve" approved="no"> - <source>History</source> - <target state="translated">Історія</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve" approved="no"> - <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> - <target state="translated">Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve" approved="no"> - <source>Image will be received when your contact completes uploading it.</source> - <target state="translated">Зображення буде отримано, коли ваш контакт завершить завантаження.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Don't create address" xml:space="preserve" approved="no"> - <source>Don't create address</source> - <target state="translated">Не створювати адресу</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Abort changing address?" xml:space="preserve" approved="no"> - <source>Abort changing address?</source> - <target state="translated">Скасувати зміну адреси?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Abort" xml:space="preserve" approved="no"> + <trans-unit id="Abort" xml:space="preserve"> <source>Abort</source> - <target state="translated">Скасувати</target> + <target>Скасувати</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Enable self-destruct" xml:space="preserve" approved="no"> - <source>Enable self-destruct</source> - <target state="translated">Увімкнути самознищення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Abort changing address" xml:space="preserve" approved="no"> + <trans-unit id="Abort changing address" xml:space="preserve"> <source>Abort changing address</source> - <target state="translated">Скасувати зміну адреси</target> + <target>Скасувати зміну адреси</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve" approved="no"> + <trans-unit id="Abort changing address?" xml:space="preserve"> + <source>Abort changing address?</source> + <target>Скасувати зміну адреси?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About SimpleX" xml:space="preserve"> + <source>About SimpleX</source> + <target>Про SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About SimpleX Chat" xml:space="preserve"> + <source>About SimpleX Chat</source> + <target>Про чат SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="About SimpleX address" xml:space="preserve"> + <source>About SimpleX address</source> + <target>Про адресу SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accent color" xml:space="preserve"> + <source>Accent color</source> + <target>Акцентний колір</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept" xml:space="preserve"> + <source>Accept</source> + <target>Прийняти</target> + <note>accept contact request via notification + accept incoming call via notification</note> + </trans-unit> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>Прийняти запит на підключення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Accept contact request from %@?" xml:space="preserve"> + <source>Accept contact request from %@?</source> + <target>Прийняти запит на контакт від %@?</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="Accept incognito" xml:space="preserve"> + <source>Accept incognito</source> + <target>Прийняти інкогніто</target> + <note>accept contact request via notification</note> + </trans-unit> + <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> + <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> + <target>Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add preset servers" xml:space="preserve"> + <source>Add preset servers</source> + <target>Додавання попередньо встановлених серверів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add profile" xml:space="preserve"> + <source>Add profile</source> + <target>Додати профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add servers by scanning QR codes." xml:space="preserve"> + <source>Add servers by scanning QR codes.</source> + <target>Додайте сервери, відсканувавши QR-код.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add server…" xml:space="preserve"> + <source>Add server…</source> + <target>Додати сервер…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add to another device" xml:space="preserve"> + <source>Add to another device</source> + <target>Додати до іншого пристрою</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Add welcome message" xml:space="preserve"> + <source>Add welcome message</source> + <target>Додати вітальне повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address" xml:space="preserve"> + <source>Address</source> + <target>Адреса</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Address change will be aborted. Old receiving address will be used." xml:space="preserve"> <source>Address change will be aborted. Old receiving address will be used.</source> - <target state="translated">Зміна адреси буде скасована. Буде використано стару адресу отримання.</target> + <target>Зміна адреси буде скасована. Буде використано стару адресу отримання.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappearing message" xml:space="preserve" approved="no"> - <source>Disappearing message</source> - <target state="translated">Зникаюче повідомлення</target> + <trans-unit id="Admins can create the links to join groups." xml:space="preserve"> + <source>Admins can create the links to join groups.</source> + <target>Адміни можуть створювати посилання для приєднання до груп.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappears at: %@" xml:space="preserve" approved="no"> - <source>Disappears at: %@</source> - <target state="translated">Зникає за: %@</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Enter welcome message… (optional)" xml:space="preserve" approved="no"> - <source>Enter welcome message… (optional)</source> - <target state="translated">Введіть вітальне повідомлення... (необов'язково)</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="Enable self-destruct passcode" xml:space="preserve" approved="no"> - <source>Enable self-destruct passcode</source> - <target state="translated">Увімкнути пароль самознищення</target> - <note>set passcode view</note> - </trans-unit> - <trans-unit id="Don't show again" xml:space="preserve" approved="no"> - <source>Don't show again</source> - <target state="translated">Більше не показувати</target> + <trans-unit id="Advanced network settings" xml:space="preserve"> + <source>Advanced network settings</source> + <target>Розширені налаштування мережі</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Downgrade and open chat" xml:space="preserve" approved="no"> - <source>Downgrade and open chat</source> - <target state="translated">Пониження та відкритий чат</target> + <trans-unit id="All app data is deleted." xml:space="preserve"> + <source>All app data is deleted.</source> + <target>Всі дані програми видаляються.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Download file" xml:space="preserve" approved="no"> - <source>Download file</source> - <target state="translated">Завантажити файл</target> - <note>server test step</note> - </trans-unit> - <trans-unit id="Enter password above to show!" xml:space="preserve" approved="no"> - <source>Enter password above to show!</source> - <target state="translated">Введіть пароль вище, щоб показати!</target> + <trans-unit id="All chats and messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>All chats and messages will be deleted - this cannot be undone!</source> + <target>Всі чати та повідомлення будуть видалені - це неможливо скасувати!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error loading %@ servers" xml:space="preserve" approved="no"> - <source>Error loading %@ servers</source> - <target state="translated">Помилка завантаження %@ серверів</target> + <trans-unit id="All data is erased when it is entered." xml:space="preserve"> + <source>All data is erased when it is entered.</source> + <target>Всі дані стираються при введенні.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving %@ servers" xml:space="preserve" approved="no"> - <source>Error saving %@ servers</source> - <target state="translated">Помилка збереження %@ серверів</target> + <trans-unit id="All group members will remain connected." xml:space="preserve"> + <source>All group members will remain connected.</source> + <target>Всі учасники групи залишаться на зв'язку.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving passcode" xml:space="preserve" approved="no"> - <source>Error saving passcode</source> - <target state="translated">Помилка збереження пароля</target> + <trans-unit id="All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." xml:space="preserve"> + <source>All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you.</source> + <target>Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error saving user password" xml:space="preserve" approved="no"> - <source>Error saving user password</source> - <target state="translated">Помилка збереження пароля користувача</target> + <trans-unit id="All your contacts will remain connected." xml:space="preserve"> + <source>All your contacts will remain connected.</source> + <target>Всі ваші контакти залишаться на зв'язку.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error: " xml:space="preserve" approved="no"> - <source>Error: </source> - <target state="translated">Помилка: </target> + <trans-unit id="All your contacts will remain connected. Profile update will be sent to your contacts." xml:space="preserve"> + <source>All your contacts will remain connected. Profile update will be sent to your contacts.</source> + <target>Всі ваші контакти залишаться на зв'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Error updating user privacy" xml:space="preserve" approved="no"> - <source>Error updating user privacy</source> - <target state="translated">Помилка оновлення конфіденційності користувача</target> + <trans-unit id="Allow" xml:space="preserve"> + <source>Allow</source> + <target>Дозволити</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve" approved="no"> - <source>Fully re-implemented - work in background!</source> - <target state="translated">Повністю перероблено - робота у фоновому режимі!</target> + <trans-unit id="Allow calls only if your contact allows them." xml:space="preserve"> + <source>Allow calls only if your contact allows them.</source> + <target>Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group moderation" xml:space="preserve" approved="no"> - <source>Group moderation</source> - <target state="translated">Модерація груп</target> + <trans-unit id="Allow disappearing messages only if your contact allows it to you." xml:space="preserve"> + <source>Allow disappearing messages only if your contact allows it to you.</source> + <target>Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Group welcome message" xml:space="preserve" approved="no"> - <source>Group welcome message</source> - <target state="translated">Привітальне повідомлення групи</target> + <trans-unit id="Allow irreversible message deletion only if your contact allows it to you." xml:space="preserve"> + <source>Allow irreversible message deletion only if your contact allows it to you.</source> + <target>Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hidden profile password" xml:space="preserve" approved="no"> - <source>Hidden profile password</source> - <target state="translated">Прихований пароль до профілю</target> + <trans-unit id="Allow message reactions only if your contact allows them." xml:space="preserve"> + <source>Allow message reactions only if your contact allows them.</source> + <target>Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hide:" xml:space="preserve" approved="no"> - <source>Hide:</source> - <target state="translated">Приховати:</target> + <trans-unit id="Allow message reactions." xml:space="preserve"> + <source>Allow message reactions.</source> + <target>Дозволити реакцію на повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Immediately" xml:space="preserve" approved="no"> - <source>Immediately</source> - <target state="translated">Негайно</target> + <trans-unit id="Allow sending direct messages to members." xml:space="preserve"> + <source>Allow sending direct messages to members.</source> + <target>Дозволяє надсилати прямі повідомлення користувачам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incorrect passcode" xml:space="preserve" approved="no"> - <source>Incorrect passcode</source> - <target state="translated">Неправильний пароль</target> - <note>PIN entry</note> - </trans-unit> - <trans-unit id="Incompatible database version" xml:space="preserve" approved="no"> - <source>Incompatible database version</source> - <target state="translated">Несумісна версія бази даних</target> + <trans-unit id="Allow sending disappearing messages." xml:space="preserve"> + <source>Allow sending disappearing messages.</source> + <target>Дозволити надсилання зникаючих повідомлень.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Initial role" xml:space="preserve" approved="no"> - <source>Initial role</source> - <target state="translated">Початкова роль</target> + <trans-unit id="Allow to irreversibly delete sent messages." xml:space="preserve"> + <source>Allow to irreversibly delete sent messages.</source> + <target>Дозволяє безповоротно видаляти надіслані повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Disappears at" xml:space="preserve" approved="no"> - <source>Disappears at</source> - <target state="translated">Зникає за</target> + <trans-unit id="Allow to send files and media." xml:space="preserve"> + <source>Allow to send files and media.</source> + <target>Дозволяє надсилати файли та медіа.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Duration" xml:space="preserve" approved="no"> - <source>Duration</source> - <target state="translated">Тривалість</target> + <trans-unit id="Allow to send voice messages." xml:space="preserve"> + <source>Allow to send voice messages.</source> + <target>Дозволити надсилати голосові повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Encrypted message: database migration error" xml:space="preserve" approved="no"> - <source>Encrypted message: database migration error</source> - <target state="translated">Зашифроване повідомлення: помилка міграції бази даних</target> - <note>notification</note> - </trans-unit> - <trans-unit id="Enter welcome message…" xml:space="preserve" approved="no"> - <source>Enter welcome message…</source> - <target state="translated">Введіть вітальне повідомлення…</target> - <note>placeholder</note> - </trans-unit> - <trans-unit id="Error sending email" xml:space="preserve" approved="no"> - <source>Error sending email</source> - <target state="translated">Помилка надсилання електронного листа</target> + <trans-unit id="Allow voice messages only if your contact allows them." xml:space="preserve"> + <source>Allow voice messages only if your contact allows them.</source> + <target>Дозволяйте голосові повідомлення, тільки якщо ваш контакт дозволяє їх.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="File will be deleted from servers." xml:space="preserve" approved="no"> - <source>File will be deleted from servers.</source> - <target state="translated">Файл буде видалено з серверів.</target> + <trans-unit id="Allow voice messages?" xml:space="preserve"> + <source>Allow voice messages?</source> + <target>Дозволити голосові повідомлення?</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve" approved="no"> - <source>Fast and no wait until the sender is online!</source> - <target state="translated">Швидко і без очікування, поки відправник буде онлайн!</target> + <trans-unit id="Allow your contacts adding message reactions." xml:space="preserve"> + <source>Allow your contacts adding message reactions.</source> + <target>Дозвольте вашим контактам додавати реакції на повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Hide profile" xml:space="preserve" approved="no"> - <source>Hide profile</source> - <target state="translated">Приховати профіль</target> + <trans-unit id="Allow your contacts to call you." xml:space="preserve"> + <source>Allow your contacts to call you.</source> + <target>Дозвольте вашим контактам телефонувати вам.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Deleted at: %@" xml:space="preserve" approved="no"> - <source>Deleted at: %@</source> - <target state="translated">Видалено за: %@</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Deleted at" xml:space="preserve" approved="no"> - <source>Deleted at</source> - <target state="translated">Видалено за</target> + <trans-unit id="Allow your contacts to irreversibly delete sent messages." xml:space="preserve"> + <source>Allow your contacts to irreversibly delete sent messages.</source> + <target>Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="KeyChain error" xml:space="preserve" approved="no"> - <source>KeyChain error</source> - <target state="translated">Помилка KeyChain</target> + <trans-unit id="Allow your contacts to send disappearing messages." xml:space="preserve"> + <source>Allow your contacts to send disappearing messages.</source> + <target>Дозвольте своїм контактам надсилати зникаючі повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Lock mode" xml:space="preserve" approved="no"> - <source>Lock mode</source> - <target state="translated">Режим блокування</target> + <trans-unit id="Allow your contacts to send voice messages." xml:space="preserve"> + <source>Allow your contacts to send voice messages.</source> + <target>Дозвольте своїм контактам надсилати голосові повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions" xml:space="preserve" approved="no"> - <source>Message reactions</source> - <target state="translated">Реакції на повідомлення</target> + <trans-unit id="Already connected?" xml:space="preserve"> + <source>Already connected?</source> + <target>Вже підключено?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Always use relay" xml:space="preserve"> + <source>Always use relay</source> + <target>Завжди використовуйте реле</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="An empty chat profile with the provided name is created, and the app opens as usual." xml:space="preserve"> + <source>An empty chat profile with the provided name is created, and the app opens as usual.</source> + <target>Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Answer call" xml:space="preserve"> + <source>Answer call</source> + <target>Відповісти на дзвінок</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App build: %@" xml:space="preserve"> + <source>App build: %@</source> + <target>Збірка програми: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App icon" xml:space="preserve"> + <source>App icon</source> + <target>Іконка програми</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode" xml:space="preserve"> + <source>App passcode</source> + <target>Пароль додатку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App passcode is replaced with self-destruct passcode." xml:space="preserve"> + <source>App passcode is replaced with self-destruct passcode.</source> + <target>Пароль програми замінено на пароль самознищення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App version" xml:space="preserve"> + <source>App version</source> + <target>Версія програми</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="App version: v%@" xml:space="preserve"> + <source>App version: v%@</source> + <target>Версія програми: v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Appearance" xml:space="preserve"> + <source>Appearance</source> + <target>Зовнішній вигляд</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Attach" xml:space="preserve"> + <source>Attach</source> + <target>Прикріпити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Audio & video calls" xml:space="preserve"> + <source>Audio & video calls</source> + <target>Аудіо та відео дзвінки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Audio and video calls" xml:space="preserve"> + <source>Audio and video calls</source> + <target>Аудіо та відеодзвінки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Audio/video calls" xml:space="preserve"> + <source>Audio/video calls</source> + <target>Аудіо/відео дзвінки</target> <note>chat feature</note> </trans-unit> - <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve" approved="no"> + <trans-unit id="Audio/video calls are prohibited." xml:space="preserve"> + <source>Audio/video calls are prohibited.</source> + <target>Аудіо/відео дзвінки заборонені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication cancelled" xml:space="preserve"> + <source>Authentication cancelled</source> + <target>Аутентифікацію скасовано</target> + <note>PIN entry</note> + </trans-unit> + <trans-unit id="Authentication failed" xml:space="preserve"> + <source>Authentication failed</source> + <target>Не вдалося пройти автентифікацію</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication is required before the call is connected, but you may miss calls." xml:space="preserve"> + <source>Authentication is required before the call is connected, but you may miss calls.</source> + <target>Перед з'єднанням дзвінка потрібно пройти автентифікацію, але ви можете пропустити дзвінки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Authentication unavailable" xml:space="preserve"> + <source>Authentication unavailable</source> + <target>Автентифікація недоступна</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept" xml:space="preserve"> + <source>Auto-accept</source> + <target>Автоприйняття</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept contact requests" xml:space="preserve"> + <source>Auto-accept contact requests</source> + <target>Автоматичне прийняття запитів на контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Auto-accept images" xml:space="preserve"> + <source>Auto-accept images</source> + <target>Автоматичне прийняття зображень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Back" xml:space="preserve"> + <source>Back</source> + <target>Назад</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad message ID" xml:space="preserve"> + <source>Bad message ID</source> + <target>Неправильний ідентифікатор повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bad message hash" xml:space="preserve"> + <source>Bad message hash</source> + <target>Поганий хеш повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Better messages" xml:space="preserve"> + <source>Better messages</source> + <target>Кращі повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can add message reactions." xml:space="preserve"> + <source>Both you and your contact can add message reactions.</source> + <target>Реакції на повідомлення можете додавати як ви, так і ваш контакт.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can irreversibly delete sent messages." xml:space="preserve"> + <source>Both you and your contact can irreversibly delete sent messages.</source> + <target>І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can make calls." xml:space="preserve"> + <source>Both you and your contact can make calls.</source> + <target>Дзвонити можете як ви, так і ваш контакт.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can send disappearing messages." xml:space="preserve"> + <source>Both you and your contact can send disappearing messages.</source> + <target>Ви і ваш контакт можете надсилати зникаючі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Both you and your contact can send voice messages." xml:space="preserve"> + <source>Both you and your contact can send voice messages.</source> + <target>Надсилати голосові повідомлення можете як ви, так і ваш контакт.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> + <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> + <target>Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Call already ended!" xml:space="preserve"> + <source>Call already ended!</source> + <target>Дзвінок вже закінчився!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Calls" xml:space="preserve"> + <source>Calls</source> + <target>Дзвінки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't delete user profile!" xml:space="preserve"> + <source>Can't delete user profile!</source> + <target>Не можу видалити профіль користувача!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't invite contact!" xml:space="preserve"> + <source>Can't invite contact!</source> + <target>Не вдається запросити контакт!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Can't invite contacts!" xml:space="preserve"> + <source>Can't invite contacts!</source> + <target>Неможливо запросити контакти!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cancel" xml:space="preserve"> + <source>Cancel</source> + <target>Скасувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot access keychain to save database password" xml:space="preserve"> + <source>Cannot access keychain to save database password</source> + <target>Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Cannot receive file" xml:space="preserve"> + <source>Cannot receive file</source> + <target>Не вдається отримати файл</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change" xml:space="preserve"> + <source>Change</source> + <target>Зміна</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change database passphrase?" xml:space="preserve"> + <source>Change database passphrase?</source> + <target>Змінити пароль до бази даних?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change lock mode" xml:space="preserve"> + <source>Change lock mode</source> + <target>Зміна режиму блокування</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change member role?" xml:space="preserve"> + <source>Change member role?</source> + <target>Змінити роль учасника?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change passcode" xml:space="preserve"> + <source>Change passcode</source> + <target>Змінити пароль</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change receiving address" xml:space="preserve"> + <source>Change receiving address</source> + <target>Змінити адресу отримання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change receiving address?" xml:space="preserve"> + <source>Change receiving address?</source> + <target>Змінити адресу отримання?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change role" xml:space="preserve"> + <source>Change role</source> + <target>Змінити роль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Change self-destruct mode" xml:space="preserve"> + <source>Change self-destruct mode</source> + <target>Змінити режим самознищення</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Change self-destruct passcode" xml:space="preserve"> + <source>Change self-destruct passcode</source> + <target>Змінити пароль самознищення</target> + <note>authentication reason + set passcode view</note> + </trans-unit> + <trans-unit id="Chat archive" xml:space="preserve"> + <source>Chat archive</source> + <target>Архів чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat console" xml:space="preserve"> + <source>Chat console</source> + <target>Консоль чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database" xml:space="preserve"> + <source>Chat database</source> + <target>База даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database deleted" xml:space="preserve"> + <source>Chat database deleted</source> + <target>Видалено базу даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat database imported" xml:space="preserve"> + <source>Chat database imported</source> + <target>Імпорт бази даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is running" xml:space="preserve"> + <source>Chat is running</source> + <target>Чат запущено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat is stopped" xml:space="preserve"> + <source>Chat is stopped</source> + <target>Чат зупинено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chat preferences" xml:space="preserve"> + <source>Chat preferences</source> + <target>Налаштування чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chats" xml:space="preserve"> + <source>Chats</source> + <target>Чати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Check server address and try again." xml:space="preserve"> + <source>Check server address and try again.</source> + <target>Перевірте адресу сервера та спробуйте ще раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Chinese and Spanish interface" xml:space="preserve"> + <source>Chinese and Spanish interface</source> + <target>Інтерфейс китайською та іспанською мовами</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose file" xml:space="preserve"> + <source>Choose file</source> + <target>Виберіть файл</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Choose from library" xml:space="preserve"> + <source>Choose from library</source> + <target>Виберіть з бібліотеки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear" xml:space="preserve"> + <source>Clear</source> + <target>Чисто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear conversation" xml:space="preserve"> + <source>Clear conversation</source> + <target>Ясна розмова</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear conversation?" xml:space="preserve"> + <source>Clear conversation?</source> + <target>Відверта розмова?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Clear verification" xml:space="preserve"> + <source>Clear verification</source> + <target>Очистити перевірку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Colors" xml:space="preserve"> + <source>Colors</source> + <target>Кольори</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Compare file" xml:space="preserve"> + <source>Compare file</source> + <target>Порівняти файл</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Compare security codes with your contacts." xml:space="preserve"> + <source>Compare security codes with your contacts.</source> + <target>Порівняйте коди безпеки зі своїми контактами.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Configure ICE servers" xml:space="preserve"> + <source>Configure ICE servers</source> + <target>Налаштування серверів ICE</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm" xml:space="preserve"> + <source>Confirm</source> + <target>Підтвердити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm Passcode" xml:space="preserve"> + <source>Confirm Passcode</source> + <target>Підтвердити пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm database upgrades" xml:space="preserve"> + <source>Confirm database upgrades</source> + <target>Підтвердити оновлення бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm new passphrase…" xml:space="preserve"> + <source>Confirm new passphrase…</source> + <target>Підтвердіть нову парольну фразу…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Confirm password" xml:space="preserve"> + <source>Confirm password</source> + <target>Підтвердити пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect" xml:space="preserve"> + <source>Connect</source> + <target>Підключіться</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>Підключіться безпосередньо</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>Підключайтеся інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>Підключіться за контактним посиланням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via group link?" xml:space="preserve"> + <source>Connect via group link?</source> + <target>Підключитися за груповим посиланням?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via link" xml:space="preserve"> + <source>Connect via link</source> + <target>Підключіться за посиланням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via link / QR code" xml:space="preserve"> + <source>Connect via link / QR code</source> + <target>Підключитися за посиланням / QR-кодом</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>Під'єднатися за одноразовим посиланням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting server…" xml:space="preserve"> + <source>Connecting to server…</source> + <target>Підключення до сервера…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connecting server… (error: %@)" xml:space="preserve"> + <source>Connecting to server… (error: %@)</source> + <target>Підключення до сервера... (помилка: %@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection" xml:space="preserve"> + <source>Connection</source> + <target>Підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection error" xml:space="preserve"> + <source>Connection error</source> + <target>Помилка підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection error (AUTH)" xml:space="preserve"> + <source>Connection error (AUTH)</source> + <target>Помилка підключення (AUTH)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection request sent!" xml:space="preserve"> + <source>Connection request sent!</source> + <target>Запит на підключення відправлено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connection timeout" xml:space="preserve"> + <source>Connection timeout</source> + <target>Тайм-аут з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact allows" xml:space="preserve"> + <source>Contact allows</source> + <target>Контакт дозволяє</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact already exists" xml:space="preserve"> + <source>Contact already exists</source> + <target>Контакт вже існує</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact and all messages will be deleted - this cannot be undone!" xml:space="preserve"> + <source>Contact and all messages will be deleted - this cannot be undone!</source> + <target>Контакт і всі повідомлення будуть видалені - це неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact hidden:" xml:space="preserve"> + <source>Contact hidden:</source> + <target>Контакт приховано:</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Contact is connected" xml:space="preserve"> + <source>Contact is connected</source> + <target>Контакт підключений</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Contact is not connected yet!" xml:space="preserve"> + <source>Contact is not connected yet!</source> + <target>Контакт ще не підключено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact name" xml:space="preserve"> + <source>Contact name</source> + <target>Ім'я контактної особи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contact preferences" xml:space="preserve"> + <source>Contact preferences</source> + <target>Налаштування контактів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contacts" xml:space="preserve"> + <source>Contacts</source> + <target>Контакти</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> + <source>Contacts can mark messages for deletion; you will be able to view them.</source> + <target>Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Continue" xml:space="preserve"> + <source>Continue</source> + <target>Продовжуйте</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Copy" xml:space="preserve"> + <source>Copy</source> + <target>Копіювати</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Core version: v%@" xml:space="preserve"> + <source>Core version: v%@</source> + <target>Основна версія: v%@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create" xml:space="preserve"> + <source>Create</source> + <target>Створити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create SimpleX address" xml:space="preserve"> + <source>Create SimpleX address</source> + <target>Створіть адресу SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create an address to let people connect with you." xml:space="preserve"> + <source>Create an address to let people connect with you.</source> + <target>Створіть адресу, щоб люди могли з вами зв'язатися.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create file" xml:space="preserve"> + <source>Create file</source> + <target>Створити файл</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Create group link" xml:space="preserve"> + <source>Create group link</source> + <target>Створити групове посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create link" xml:space="preserve"> + <source>Create link</source> + <target>Створити посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create one-time invitation link" xml:space="preserve"> + <source>Create one-time invitation link</source> + <target>Створіть одноразове посилання-запрошення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create queue" xml:space="preserve"> + <source>Create queue</source> + <target>Створити чергу</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Create secret group" xml:space="preserve"> + <source>Create secret group</source> + <target>Створити секретну групу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Create your profile" xml:space="preserve"> + <source>Create your profile</source> + <target>Створіть свій профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Created on %@" xml:space="preserve"> + <source>Created on %@</source> + <target>Створено %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current Passcode" xml:space="preserve"> + <source>Current Passcode</source> + <target>Поточний пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Current passphrase…" xml:space="preserve"> + <source>Current passphrase…</source> + <target>Поточна парольна фраза…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Currently maximum supported file size is %@." xml:space="preserve"> + <source>Currently maximum supported file size is %@.</source> + <target>Наразі максимальний підтримуваний розмір файлу - %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Custom time" xml:space="preserve"> + <source>Custom time</source> + <target>Індивідуальний час</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Dark" xml:space="preserve"> + <source>Dark</source> + <target>Темний</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database ID" xml:space="preserve"> + <source>Database ID</source> + <target>Ідентифікатор бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database ID: %d" xml:space="preserve"> + <source>Database ID: %d</source> + <target>Ідентифікатор бази даних: %d</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Database IDs and Transport isolation option." xml:space="preserve"> + <source>Database IDs and Transport isolation option.</source> + <target>Ідентифікатори бази даних та опція ізоляції транспорту.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database downgrade" xml:space="preserve"> + <source>Database downgrade</source> + <target>Пониження версії бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encrypted!" xml:space="preserve"> + <source>Database encrypted!</source> + <target>База даних зашифрована!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encryption passphrase will be updated and stored in the keychain. " xml:space="preserve"> + <source>Database encryption passphrase will be updated and stored in the keychain. +</source> + <target>Парольна фраза шифрування бази даних буде оновлена та збережена у в’язці ключів. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database encryption passphrase will be updated. " xml:space="preserve"> + <source>Database encryption passphrase will be updated. +</source> + <target>Ключову фразу шифрування бази даних буде оновлено. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database error" xml:space="preserve"> + <source>Database error</source> + <target>Помилка в базі даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database is encrypted using a random passphrase, you can change it." xml:space="preserve"> + <source>Database is encrypted using a random passphrase, you can change it.</source> + <target>База даних зашифрована за допомогою випадкової парольної фрази, яку ви можете змінити.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database is encrypted using a random passphrase. Please change it before exporting." xml:space="preserve"> + <source>Database is encrypted using a random passphrase. Please change it before exporting.</source> + <target>База даних зашифрована за допомогою випадкової парольної фрази. Будь ласка, змініть його перед експортом.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase" xml:space="preserve"> + <source>Database passphrase</source> + <target>Ключова фраза бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase & export" xml:space="preserve"> + <source>Database passphrase & export</source> + <target>Ключова фраза бази даних та експорт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is different from saved in the keychain." xml:space="preserve"> + <source>Database passphrase is different from saved in the keychain.</source> + <target>Парольна фраза бази даних відрізняється від збереженої у в’язці ключів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database passphrase is required to open chat." xml:space="preserve"> + <source>Database passphrase is required to open chat.</source> + <target>Для відкриття чату потрібно ввести пароль до бази даних.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database upgrade" xml:space="preserve"> + <source>Database upgrade</source> + <target>Оновлення бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database will be encrypted and the passphrase stored in the keychain. " xml:space="preserve"> + <source>Database will be encrypted and the passphrase stored in the keychain. +</source> + <target>База даних буде зашифрована, а парольна фраза збережена у в’язці ключів. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database will be encrypted. " xml:space="preserve"> + <source>Database will be encrypted. +</source> + <target>База даних буде зашифрована. +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Database will be migrated when the app restarts" xml:space="preserve"> + <source>Database will be migrated when the app restarts</source> + <target>База даних буде перенесена під час перезапуску програми</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Decentralized" xml:space="preserve"> + <source>Decentralized</source> + <target>Децентралізований</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Decryption error" xml:space="preserve"> + <source>Decryption error</source> + <target>Помилка розшифровки</target> + <note>message decrypt error item</note> + </trans-unit> + <trans-unit id="Delete" xml:space="preserve"> + <source>Delete</source> + <target>Видалити</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Delete Contact" xml:space="preserve"> + <source>Delete Contact</source> + <target>Видалити контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete address" xml:space="preserve"> + <source>Delete address</source> + <target>Видалити адресу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete address?" xml:space="preserve"> + <source>Delete address?</source> + <target>Видалити адресу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete after" xml:space="preserve"> + <source>Delete after</source> + <target>Видалити після</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete all files" xml:space="preserve"> + <source>Delete all files</source> + <target>Видалити всі файли</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete archive" xml:space="preserve"> + <source>Delete archive</source> + <target>Видалити архів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat archive?" xml:space="preserve"> + <source>Delete chat archive?</source> + <target>Видалити архів чату?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat profile" xml:space="preserve"> + <source>Delete chat profile</source> + <target>Видалити профіль чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete chat profile?" xml:space="preserve"> + <source>Delete chat profile?</source> + <target>Видалити профіль чату?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete connection" xml:space="preserve"> + <source>Delete connection</source> + <target>Видалити підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete contact" xml:space="preserve"> + <source>Delete contact</source> + <target>Видалити контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete contact?" xml:space="preserve"> + <source>Delete contact?</source> + <target>Видалити контакт?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete database" xml:space="preserve"> + <source>Delete database</source> + <target>Видалити базу даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete file" xml:space="preserve"> + <source>Delete file</source> + <target>Видалити файл</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Delete files and media?" xml:space="preserve"> + <source>Delete files and media?</source> + <target>Видаляти файли та медіа?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete files for all chat profiles" xml:space="preserve"> + <source>Delete files for all chat profiles</source> + <target>Видалення файлів для всіх профілів чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete for everyone" xml:space="preserve"> + <source>Delete for everyone</source> + <target>Видалити для всіх</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Delete for me" xml:space="preserve"> + <source>Delete for me</source> + <target>Видалити для мене</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete group" xml:space="preserve"> + <source>Delete group</source> + <target>Видалити групу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete group?" xml:space="preserve"> + <source>Delete group?</source> + <target>Видалити групу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete invitation" xml:space="preserve"> + <source>Delete invitation</source> + <target>Видалити запрошення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete link" xml:space="preserve"> + <source>Delete link</source> + <target>Видалити посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete link?" xml:space="preserve"> + <source>Delete link?</source> + <target>Видалити посилання?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete member message?" xml:space="preserve"> + <source>Delete member message?</source> + <target>Видалити повідомлення учасника?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete message?" xml:space="preserve"> + <source>Delete message?</source> + <target>Видалити повідомлення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete messages" xml:space="preserve"> + <source>Delete messages</source> + <target>Видалити повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete messages after" xml:space="preserve"> + <source>Delete messages after</source> + <target>Видаляйте повідомлення після</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete old database" xml:space="preserve"> + <source>Delete old database</source> + <target>Видалення старої бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete old database?" xml:space="preserve"> + <source>Delete old database?</source> + <target>Видалити стару базу даних?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete pending connection" xml:space="preserve"> + <source>Delete pending connection</source> + <target>Видалити очікуване з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete pending connection?" xml:space="preserve"> + <source>Delete pending connection?</source> + <target>Видалити очікуване з'єднання?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete profile" xml:space="preserve"> + <source>Delete profile</source> + <target>Видалити профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delete queue" xml:space="preserve"> + <source>Delete queue</source> + <target>Видалити чергу</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Delete user profile?" xml:space="preserve"> + <source>Delete user profile?</source> + <target>Видалити профіль користувача?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted at" xml:space="preserve"> + <source>Deleted at</source> + <target>Видалено за</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Deleted at: %@" xml:space="preserve"> + <source>Deleted at: %@</source> + <target>Видалено за: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>Доставка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> + <source>Delivery receipts are disabled!</source> + <target>Квитанції про доставку відключені!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Delivery receipts!" xml:space="preserve"> + <source>Delivery receipts!</source> + <target>Квитанції про доставку!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Description" xml:space="preserve"> + <source>Description</source> + <target>Опис</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Develop" xml:space="preserve"> + <source>Develop</source> + <target>Розробник</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Developer tools" xml:space="preserve"> + <source>Developer tools</source> + <target>Інструменти для розробників</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Device" xml:space="preserve"> + <source>Device</source> + <target>Пристрій</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Device authentication is disabled. Turning off SimpleX Lock." xml:space="preserve"> + <source>Device authentication is disabled. Turning off SimpleX Lock.</source> + <target>Автентифікацію пристрою вимкнено. Вимкнення SimpleX Lock.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." xml:space="preserve"> + <source>Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication.</source> + <target>Автентифікація пристрою не ввімкнена. Ви можете увімкнути SimpleX Lock у Налаштуваннях, коли увімкнете автентифікацію пристрою.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Different names, avatars and transport isolation." xml:space="preserve"> + <source>Different names, avatars and transport isolation.</source> + <target>Різні імена, аватарки та транспортна ізоляція.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Direct messages" xml:space="preserve"> + <source>Direct messages</source> + <target>Прямі повідомлення</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Direct messages between members are prohibited in this group." xml:space="preserve"> + <source>Direct messages between members are prohibited in this group.</source> + <target>У цій групі заборонені прямі повідомлення між учасниками.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable (keep overrides)" xml:space="preserve"> + <source>Disable (keep overrides)</source> + <target>Вимкнути (зберегти перевизначення)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> + <source>Disable SimpleX Lock</source> + <target>Вимкнути SimpleX Lock</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Disable for all" xml:space="preserve"> + <source>Disable for all</source> + <target>Вимкнути для всіх</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing message" xml:space="preserve"> + <source>Disappearing message</source> + <target>Зникаюче повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing messages" xml:space="preserve"> + <source>Disappearing messages</source> + <target>Зникаючі повідомлення</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Disappearing messages are prohibited in this chat." xml:space="preserve"> + <source>Disappearing messages are prohibited in this chat.</source> + <target>Зникаючі повідомлення в цьому чаті заборонені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappearing messages are prohibited in this group." xml:space="preserve"> + <source>Disappearing messages are prohibited in this group.</source> + <target>У цій групі заборонено зникаючі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappears at" xml:space="preserve"> + <source>Disappears at</source> + <target>Зникає за</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Disappears at: %@" xml:space="preserve"> + <source>Disappears at: %@</source> + <target>Зникає за: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Disconnect" xml:space="preserve"> + <source>Disconnect</source> + <target>Від'єднати</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Display name" xml:space="preserve"> + <source>Display name</source> + <target>Відображуване ім'я</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Display name:" xml:space="preserve"> + <source>Display name:</source> + <target>Відображуване ім'я:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do NOT use SimpleX for emergency calls." xml:space="preserve"> + <source>Do NOT use SimpleX for emergency calls.</source> + <target>НЕ використовуйте SimpleX для екстрених викликів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Do it later" xml:space="preserve"> + <source>Do it later</source> + <target>Зробіть це пізніше</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't create address" xml:space="preserve"> + <source>Don't create address</source> + <target>Не створювати адресу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't enable" xml:space="preserve"> + <source>Don't enable</source> + <target>Не вмикати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Don't show again" xml:space="preserve"> + <source>Don't show again</source> + <target>Більше не показувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Downgrade and open chat" xml:space="preserve"> + <source>Downgrade and open chat</source> + <target>Пониження та відкритий чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Download file" xml:space="preserve"> + <source>Download file</source> + <target>Завантажити файл</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Duplicate display name!" xml:space="preserve"> + <source>Duplicate display name!</source> + <target>Дублююче ім'я користувача!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Duration" xml:space="preserve"> + <source>Duration</source> + <target>Тривалість</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Edit" xml:space="preserve"> + <source>Edit</source> + <target>Редагувати</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Edit group profile" xml:space="preserve"> + <source>Edit group profile</source> + <target>Редагування профілю групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable" xml:space="preserve"> + <source>Enable</source> + <target>Увімкнути</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable (keep overrides)" xml:space="preserve"> + <source>Enable (keep overrides)</source> + <target>Увімкнути (зберегти перевизначення)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> + <source>Enable SimpleX Lock</source> + <target>Увімкнути SimpleX Lock</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Enable TCP keep-alive" xml:space="preserve"> + <source>Enable TCP keep-alive</source> + <target>Увімкнути TCP keep-alive</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable automatic message deletion?" xml:space="preserve"> + <source>Enable automatic message deletion?</source> + <target>Увімкнути автоматичне видалення повідомлень?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable for all" xml:space="preserve"> + <source>Enable for all</source> + <target>Увімкнути для всіх</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable instant notifications?" xml:space="preserve"> + <source>Enable instant notifications?</source> + <target>Увімкнути миттєві сповіщення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable lock" xml:space="preserve"> + <source>Enable lock</source> + <target>Увімкнути блокування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable notifications" xml:space="preserve"> + <source>Enable notifications</source> + <target>Увімкнути сповіщення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable periodic notifications?" xml:space="preserve"> + <source>Enable periodic notifications?</source> + <target>Увімкнути періодичні сповіщення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable self-destruct" xml:space="preserve"> + <source>Enable self-destruct</source> + <target>Увімкнути самознищення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enable self-destruct passcode" xml:space="preserve"> + <source>Enable self-destruct passcode</source> + <target>Увімкнути пароль самознищення</target> + <note>set passcode view</note> + </trans-unit> + <trans-unit id="Encrypt" xml:space="preserve"> + <source>Encrypt</source> + <target>Зашифрувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt database?" xml:space="preserve"> + <source>Encrypt database?</source> + <target>Зашифрувати базу даних?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypted database" xml:space="preserve"> + <source>Encrypted database</source> + <target>Зашифрована база даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypted message or another event" xml:space="preserve"> + <source>Encrypted message or another event</source> + <target>Зашифроване повідомлення або інша подія</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: database error" xml:space="preserve"> + <source>Encrypted message: database error</source> + <target>Зашифроване повідомлення: помилка бази даних</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: database migration error" xml:space="preserve"> + <source>Encrypted message: database migration error</source> + <target>Зашифроване повідомлення: помилка міграції бази даних</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: keychain error" xml:space="preserve"> + <source>Encrypted message: keychain error</source> + <target>Зашифроване повідомлення: помилка ланцюжка ключів</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: no passphrase" xml:space="preserve"> + <source>Encrypted message: no passphrase</source> + <target>Зашифроване повідомлення: без ключової фрази</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Encrypted message: unexpected error" xml:space="preserve"> + <source>Encrypted message: unexpected error</source> + <target>Зашифроване повідомлення: несподівана помилка</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Enter Passcode" xml:space="preserve"> + <source>Enter Passcode</source> + <target>Введіть пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter correct passphrase." xml:space="preserve"> + <source>Enter correct passphrase.</source> + <target>Введіть правильну парольну фразу.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter passphrase…" xml:space="preserve"> + <source>Enter passphrase…</source> + <target>Введіть пароль…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter password above to show!" xml:space="preserve"> + <source>Enter password above to show!</source> + <target>Введіть пароль вище, щоб показати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter server manually" xml:space="preserve"> + <source>Enter server manually</source> + <target>Увійдіть на сервер вручну</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Enter welcome message…" xml:space="preserve"> + <source>Enter welcome message…</source> + <target>Введіть вітальне повідомлення…</target> + <note>placeholder</note> + </trans-unit> + <trans-unit id="Enter welcome message… (optional)" xml:space="preserve"> + <source>Enter welcome message… (optional)</source> + <target>Введіть вітальне повідомлення... (необов'язково)</target> + <note>placeholder</note> + </trans-unit> + <trans-unit id="Error" xml:space="preserve"> + <source>Error</source> + <target>Помилка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error aborting address change" xml:space="preserve"> + <source>Error aborting address change</source> + <target>Помилка скасування зміни адреси</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error accepting contact request" xml:space="preserve"> + <source>Error accepting contact request</source> + <target>Помилка при прийнятті запиту на контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error accessing database file" xml:space="preserve"> + <source>Error accessing database file</source> + <target>Помилка доступу до файлу бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error adding member(s)" xml:space="preserve"> + <source>Error adding member(s)</source> + <target>Помилка додавання користувача(ів)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing address" xml:space="preserve"> + <source>Error changing address</source> + <target>Помилка зміни адреси</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing role" xml:space="preserve"> + <source>Error changing role</source> + <target>Помилка зміни ролі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error changing setting" xml:space="preserve"> + <source>Error changing setting</source> + <target>Помилка зміни налаштування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating address" xml:space="preserve"> + <source>Error creating address</source> + <target>Помилка створення адреси</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating group" xml:space="preserve"> + <source>Error creating group</source> + <target>Помилка створення групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating group link" xml:space="preserve"> + <source>Error creating group link</source> + <target>Помилка створення посилання на групу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error creating profile!" xml:space="preserve"> + <source>Error creating profile!</source> + <target>Помилка створення профілю!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting chat database" xml:space="preserve"> + <source>Error deleting chat database</source> + <target>Помилка видалення бази даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting chat!" xml:space="preserve"> + <source>Error deleting chat!</source> + <target>Помилка видалення чату!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting connection" xml:space="preserve"> + <source>Error deleting connection</source> + <target>Помилка видалення з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting contact" xml:space="preserve"> + <source>Error deleting contact</source> + <target>Помилка видалення контакту</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting database" xml:space="preserve"> + <source>Error deleting database</source> + <target>Помилка видалення бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting old database" xml:space="preserve"> + <source>Error deleting old database</source> + <target>Помилка видалення старої бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting token" xml:space="preserve"> + <source>Error deleting token</source> + <target>Помилка видалення токена</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error deleting user profile" xml:space="preserve"> + <source>Error deleting user profile</source> + <target>Помилка видалення профілю користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> + <source>Error enabling delivery receipts!</source> + <target>Помилка активації підтвердження доставлення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error enabling notifications" xml:space="preserve"> + <source>Error enabling notifications</source> + <target>Помилка увімкнення сповіщень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error encrypting database" xml:space="preserve"> + <source>Error encrypting database</source> + <target>Помилка шифрування бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error exporting chat database" xml:space="preserve"> + <source>Error exporting chat database</source> + <target>Помилка експорту бази даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error importing chat database" xml:space="preserve"> + <source>Error importing chat database</source> + <target>Помилка імпорту бази даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error joining group" xml:space="preserve"> + <source>Error joining group</source> + <target>Помилка приєднання до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error loading %@ servers" xml:space="preserve"> + <source>Error loading %@ servers</source> + <target>Помилка завантаження %@ серверів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error receiving file" xml:space="preserve"> + <source>Error receiving file</source> + <target>Помилка отримання файлу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error removing member" xml:space="preserve"> + <source>Error removing member</source> + <target>Помилка видалення учасника</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving %@ servers" xml:space="preserve"> + <source>Error saving %@ servers</source> + <target>Помилка збереження %@ серверів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving ICE servers" xml:space="preserve"> + <source>Error saving ICE servers</source> + <target>Помилка збереження серверів ICE</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving group profile" xml:space="preserve"> + <source>Error saving group profile</source> + <target>Помилка збереження профілю групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving passcode" xml:space="preserve"> + <source>Error saving passcode</source> + <target>Помилка збереження пароля</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving passphrase to keychain" xml:space="preserve"> + <source>Error saving passphrase to keychain</source> + <target>Помилка збереження пароля на keychain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error saving user password" xml:space="preserve"> + <source>Error saving user password</source> + <target>Помилка збереження пароля користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending email" xml:space="preserve"> + <source>Error sending email</source> + <target>Помилка надсилання електронного листа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error sending message" xml:space="preserve"> + <source>Error sending message</source> + <target>Помилка надсилання повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> + <source>Error setting delivery receipts!</source> + <target>Помилка встановлення підтвердження доставлення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error starting chat" xml:space="preserve"> + <source>Error starting chat</source> + <target>Помилка запуску чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error stopping chat" xml:space="preserve"> + <source>Error stopping chat</source> + <target>Помилка зупинки чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error switching profile!" xml:space="preserve"> + <source>Error switching profile!</source> + <target>Помилка перемикання профілю!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error synchronizing connection" xml:space="preserve"> + <source>Error synchronizing connection</source> + <target>Помилка синхронізації з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating group link" xml:space="preserve"> + <source>Error updating group link</source> + <target>Помилка оновлення посилання на групу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating message" xml:space="preserve"> + <source>Error updating message</source> + <target>Повідомлення про помилку оновлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating settings" xml:space="preserve"> + <source>Error updating settings</source> + <target>Помилка оновлення налаштувань</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error updating user privacy" xml:space="preserve"> + <source>Error updating user privacy</source> + <target>Помилка оновлення конфіденційності користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: " xml:space="preserve"> + <source>Error: </source> + <target>Помилка: </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: %@" xml:space="preserve"> + <source>Error: %@</source> + <target>Помилка: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: URL is invalid" xml:space="preserve"> + <source>Error: URL is invalid</source> + <target>Помилка: URL-адреса невірна</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Error: no database file" xml:space="preserve"> + <source>Error: no database file</source> + <target>Помилка: немає файлу бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> + <source>Even when disabled in the conversation.</source> + <target>Навіть коли вимкнений у розмові.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exit without saving" xml:space="preserve"> + <source>Exit without saving</source> + <target>Вихід без збереження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Export database" xml:space="preserve"> + <source>Export database</source> + <target>Експорт бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Export error:" xml:space="preserve"> + <source>Export error:</source> + <target>Помилка експорту:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exported database archive." xml:space="preserve"> + <source>Exported database archive.</source> + <target>Експортований архів бази даних.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Exporting database archive…" xml:space="preserve"> + <source>Exporting database archive…</source> + <target>Експорт архіву бази даних…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Failed to remove passphrase" xml:space="preserve"> + <source>Failed to remove passphrase</source> + <target>Не вдалося видалити парольну фразу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fast and no wait until the sender is online!" xml:space="preserve"> + <source>Fast and no wait until the sender is online!</source> + <target>Швидко і без очікування, поки відправник буде онлайн!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Favorite" xml:space="preserve"> + <source>Favorite</source> + <target>Улюблений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be deleted from servers." xml:space="preserve"> + <source>File will be deleted from servers.</source> + <target>Файл буде видалено з серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be received when your contact completes uploading it." xml:space="preserve"> + <source>File will be received when your contact completes uploading it.</source> + <target>Файл буде отримано, коли ваш контакт завершить завантаження.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>File will be received when your contact is online, please wait or check later!</source> + <target>Файл буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="File: %@" xml:space="preserve"> + <source>File: %@</source> + <target>Файл: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files & media" xml:space="preserve"> + <source>Files & media</source> + <target>Файли та медіа</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media" xml:space="preserve"> + <source>Files and media</source> + <target>Файли і медіа</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Files and media are prohibited in this group." xml:space="preserve"> + <source>Files and media are prohibited in this group.</source> + <target>Файли та медіа в цій групі заборонені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Files and media prohibited!" xml:space="preserve"> + <source>Files and media prohibited!</source> + <target>Файли та медіа заборонені!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> + <source>Filter unread and favorite chats.</source> + <target>Фільтруйте непрочитані та улюблені чати.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> + <source>Finally, we have them! 🚀</source> + <target>Нарешті, вони у нас є! 🚀</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Find chats faster" xml:space="preserve"> + <source>Find chats faster</source> + <target>Швидше знаходьте чати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix" xml:space="preserve"> + <source>Fix</source> + <target>Виправити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection" xml:space="preserve"> + <source>Fix connection</source> + <target>Виправити з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix connection?" xml:space="preserve"> + <source>Fix connection?</source> + <target>Полагодити зв'язок?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> + <source>Fix encryption after restoring backups.</source> + <target>Виправити шифрування після відновлення резервних копій.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by contact" xml:space="preserve"> + <source>Fix not supported by contact</source> + <target>Виправлення не підтримується контактом</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fix not supported by group member" xml:space="preserve"> + <source>Fix not supported by group member</source> + <target>Виправлення не підтримується учасником групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="For console" xml:space="preserve"> + <source>For console</source> + <target>Для консолі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="French interface" xml:space="preserve"> + <source>French interface</source> + <target>Французький інтерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Full link" xml:space="preserve"> + <source>Full link</source> + <target>Повне посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Full name (optional)" xml:space="preserve"> + <source>Full name (optional)</source> + <target>Повне ім'я (необов'язково)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Full name:" xml:space="preserve"> + <source>Full name:</source> + <target>Повне ім'я:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Fully re-implemented - work in background!" xml:space="preserve"> + <source>Fully re-implemented - work in background!</source> + <target>Повністю перероблено - робота у фоновому режимі!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Further reduced battery usage" xml:space="preserve"> + <source>Further reduced battery usage</source> + <target>Подальше зменшення використання акумулятора</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="GIFs and stickers" xml:space="preserve"> + <source>GIFs and stickers</source> + <target>GIF-файли та наклейки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group" xml:space="preserve"> + <source>Group</source> + <target>Група</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group display name" xml:space="preserve"> + <source>Group display name</source> + <target>Назва групи для відображення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group full name (optional)" xml:space="preserve"> + <source>Group full name (optional)</source> + <target>Повна назва групи (необов'язково)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group image" xml:space="preserve"> + <source>Group image</source> + <target>Зображення групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group invitation" xml:space="preserve"> + <source>Group invitation</source> + <target>Групове запрошення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group invitation expired" xml:space="preserve"> + <source>Group invitation expired</source> + <target>Термін дії групового запрошення закінчився</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group invitation is no longer valid, it was removed by sender." xml:space="preserve"> + <source>Group invitation is no longer valid, it was removed by sender.</source> + <target>Групове запрошення більше не дійсне, воно було видалено відправником.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group link" xml:space="preserve"> + <source>Group link</source> + <target>Посилання на групу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group links" xml:space="preserve"> + <source>Group links</source> + <target>Групові посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can add message reactions." xml:space="preserve"> + <source>Group members can add message reactions.</source> + <target>Учасники групи можуть додавати реакції на повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can irreversibly delete sent messages." xml:space="preserve"> + <source>Group members can irreversibly delete sent messages.</source> + <target>Учасники групи можуть безповоротно видаляти надіслані повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can send direct messages." xml:space="preserve"> + <source>Group members can send direct messages.</source> + <target>Учасники групи можуть надсилати прямі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can send disappearing messages." xml:space="preserve"> + <source>Group members can send disappearing messages.</source> + <target>Учасники групи можуть надсилати зникаючі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can send files and media." xml:space="preserve"> + <source>Group members can send files and media.</source> + <target>Учасники групи можуть надсилати файли та медіа.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group members can send voice messages." xml:space="preserve"> + <source>Group members can send voice messages.</source> + <target>Учасники групи можуть надсилати голосові повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group message:" xml:space="preserve"> + <source>Group message:</source> + <target>Групове повідомлення:</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Group moderation" xml:space="preserve"> + <source>Group moderation</source> + <target>Модерація груп</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group preferences" xml:space="preserve"> + <source>Group preferences</source> + <target>Параметри груп</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group profile" xml:space="preserve"> + <source>Group profile</source> + <target>Профіль групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group profile is stored on members' devices, not on the servers." xml:space="preserve"> + <source>Group profile is stored on members' devices, not on the servers.</source> + <target>Профіль групи зберігається на пристроях учасників, а не на серверах.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group welcome message" xml:space="preserve"> + <source>Group welcome message</source> + <target>Привітальне повідомлення групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group will be deleted for all members - this cannot be undone!" xml:space="preserve"> + <source>Group will be deleted for all members - this cannot be undone!</source> + <target>Група буде видалена для всіх учасників - це неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Group will be deleted for you - this cannot be undone!" xml:space="preserve"> + <source>Group will be deleted for you - this cannot be undone!</source> + <target>Група буде видалена для вас - це не може бути скасовано!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Help" xml:space="preserve"> + <source>Help</source> + <target>Довідка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hidden" xml:space="preserve"> + <source>Hidden</source> + <target>Приховано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hidden chat profiles" xml:space="preserve"> + <source>Hidden chat profiles</source> + <target>Приховані профілі чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hidden profile password" xml:space="preserve"> + <source>Hidden profile password</source> + <target>Прихований пароль до профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide" xml:space="preserve"> + <source>Hide</source> + <target>Приховати</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Hide app screen in the recent apps." xml:space="preserve"> + <source>Hide app screen in the recent apps.</source> + <target>Приховати екран програми в останніх програмах.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide profile" xml:space="preserve"> + <source>Hide profile</source> + <target>Приховати профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Hide:" xml:space="preserve"> + <source>Hide:</source> + <target>Приховати:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="History" xml:space="preserve"> + <source>History</source> + <target>Історія</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How SimpleX works" xml:space="preserve"> + <source>How SimpleX works</source> + <target>Як працює SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How it works" xml:space="preserve"> + <source>How it works</source> + <target>Як це працює</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How to" xml:space="preserve"> + <source>How to</source> + <target>Як зробити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How to use it" xml:space="preserve"> + <source>How to use it</source> + <target>Як ним користуватися</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="How to use your servers" xml:space="preserve"> + <source>How to use your servers</source> + <target>Як користуватися вашими серверами</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="ICE servers (one per line)" xml:space="preserve"> + <source>ICE servers (one per line)</source> + <target>Сервери ICE (по одному на лінію)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you can't meet in person, show QR code in a video call, or share the link." xml:space="preserve"> + <source>If you can't meet in person, show QR code in a video call, or share the link.</source> + <target>Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." xml:space="preserve"> + <source>If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link.</source> + <target>Якщо ви не можете зустрітися особисто, ви можете **сканувати QR-код у відеодзвінку**, або ваш контакт може поділитися посиланням на запрошення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you enter this passcode when opening the app, all app data will be irreversibly removed!" xml:space="preserve"> + <source>If you enter this passcode when opening the app, all app data will be irreversibly removed!</source> + <target>Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you enter your self-destruct passcode while opening the app:" xml:space="preserve"> + <source>If you enter your self-destruct passcode while opening the app:</source> + <target>Якщо ви введете пароль самознищення під час відкриття програми:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." xml:space="preserve"> + <source>If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app).</source> + <target>Якщо вам потрібно скористатися чатом зараз, натисніть **Зробити це пізніше** нижче (вам буде запропоновано перенести базу даних при перезапуску програми).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Ignore" xml:space="preserve"> + <source>Ignore</source> + <target>Ігнорувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Image will be received when your contact completes uploading it." xml:space="preserve"> + <source>Image will be received when your contact completes uploading it.</source> + <target>Зображення буде отримано, коли ваш контакт завершить завантаження.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Image will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>Image will be received when your contact is online, please wait or check later!</source> + <target>Зображення буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Immediately" xml:space="preserve"> + <source>Immediately</source> + <target>Негайно</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Immune to spam and abuse" xml:space="preserve"> + <source>Immune to spam and abuse</source> + <target>Імунітет до спаму та зловживань</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import" xml:space="preserve"> + <source>Import</source> + <target>Імпорт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import chat database?" xml:space="preserve"> + <source>Import chat database?</source> + <target>Імпортувати базу даних чату?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Import database" xml:space="preserve"> + <source>Import database</source> + <target>Імпорт бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved privacy and security" xml:space="preserve"> + <source>Improved privacy and security</source> + <target>Покращена конфіденційність та безпека</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Improved server configuration" xml:space="preserve"> + <source>Improved server configuration</source> + <target>Покращена конфігурація сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="In reply to" xml:space="preserve"> + <source>In reply to</source> + <target>У відповідь на</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito" xml:space="preserve"> + <source>Incognito</source> + <target>Інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito mode" xml:space="preserve"> + <source>Incognito mode</source> + <target>Режим інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>Режим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incoming audio call" xml:space="preserve"> + <source>Incoming audio call</source> + <target>Вхідний аудіовиклик</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Incoming call" xml:space="preserve"> + <source>Incoming call</source> + <target>Вхідний дзвінок</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Incoming video call" xml:space="preserve"> + <source>Incoming video call</source> + <target>Вхідний відеодзвінок</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Incompatible database version" xml:space="preserve"> + <source>Incompatible database version</source> + <target>Несумісна версія бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Incorrect passcode" xml:space="preserve"> + <source>Incorrect passcode</source> + <target>Неправильний пароль</target> + <note>PIN entry</note> + </trans-unit> + <trans-unit id="Incorrect security code!" xml:space="preserve"> + <source>Incorrect security code!</source> + <target>Неправильний код безпеки!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Info" xml:space="preserve"> + <source>Info</source> + <target>Інформація</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Initial role" xml:space="preserve"> + <source>Initial role</source> + <target>Початкова роль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> + <source>Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)</source> + <target>Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Instant push notifications will be hidden! " xml:space="preserve"> + <source>Instant push notifications will be hidden! +</source> + <target>Миттєві пуш-сповіщення будуть приховані! +</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Instantly" xml:space="preserve"> + <source>Instantly</source> + <target>Миттєво</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Interface" xml:space="preserve"> + <source>Interface</source> + <target>Інтерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid connection link" xml:space="preserve"> + <source>Invalid connection link</source> + <target>Неправильне посилання для підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid server address!" xml:space="preserve"> + <source>Invalid server address!</source> + <target>Неправильна адреса сервера!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>Недійсний статус</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Invitation expired!" xml:space="preserve"> + <source>Invitation expired!</source> + <target>Термін дії запрошення закінчився!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite friends" xml:space="preserve"> + <source>Invite friends</source> + <target>Запросити друзів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite members" xml:space="preserve"> + <source>Invite members</source> + <target>Запросити учасників</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Invite to group" xml:space="preserve"> + <source>Invite to group</source> + <target>Запросити до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Irreversible message deletion" xml:space="preserve"> + <source>Irreversible message deletion</source> + <target>Безповоротне видалення повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Irreversible message deletion is prohibited in this chat." xml:space="preserve"> + <source>Irreversible message deletion is prohibited in this chat.</source> + <target>У цьому чаті заборонено безповоротне видалення повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Irreversible message deletion is prohibited in this group." xml:space="preserve"> + <source>Irreversible message deletion is prohibited in this group.</source> + <target>У цій групі заборонено безповоротне видалення повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It allows having many anonymous connections without any shared data between them in a single chat profile." xml:space="preserve"> + <source>It allows having many anonymous connections without any shared data between them in a single chat profile.</source> + <target>Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="It can happen when you or your connection used the old database backup." xml:space="preserve"> <source>It can happen when you or your connection used the old database backup.</source> - <target state="translated">Це може статися, якщо ви або ваше з'єднання використовували стару резервну копію бази даних.</target> + <target>Це може статися, якщо ви або ваше з'єднання використовували стару резервну копію бази даних.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Learn more" xml:space="preserve" approved="no"> - <source>Learn more</source> - <target state="translated">Дізнайтеся більше</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Lock after" xml:space="preserve" approved="no"> - <source>Lock after</source> - <target state="translated">Блокування після</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve" approved="no"> - <source>Let's talk in SimpleX Chat</source> - <target state="translated">Поговоримо в чаті SimpleX</target> - <note>email subject</note> - </trans-unit> - <trans-unit id="Japanese interface" xml:space="preserve" approved="no"> - <source>Japanese interface</source> - <target state="translated">Японський інтерфейс</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Make profile private!" xml:space="preserve" approved="no"> - <source>Make profile private!</source> - <target state="translated">Зробіть профіль приватним!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve" approved="no"> + <trans-unit id="It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised." xml:space="preserve"> <source>It can happen when: 1. The messages expired in the sending client after 2 days or on the server after 30 days. 2. Message decryption failed, because you or your contact used old database backup. 3. The connection was compromised.</source> - <target state="translated">Це може статися, коли: + <target>Це може статися, коли: 1. Термін дії повідомлень закінчився в клієнті-відправнику через 2 дні або на сервері через 30 днів. 2. Не вдалося розшифрувати повідомлення, тому що ви або ваш контакт використовували стару резервну копію бази даних. 3. З'єднання було скомпрометовано.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve" approved="no"> + <trans-unit id="It seems like you are already connected via this link. If it is not the case, there was an error (%@)." xml:space="preserve"> + <source>It seems like you are already connected via this link. If it is not the case, there was an error (%@).</source> + <target>Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Italian interface" xml:space="preserve"> + <source>Italian interface</source> + <target>Італійський інтерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Japanese interface" xml:space="preserve"> + <source>Japanese interface</source> + <target>Японський інтерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join" xml:space="preserve"> + <source>Join</source> + <target>Приєднуйтесь</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join group" xml:space="preserve"> + <source>Join group</source> + <target>Приєднуйтесь до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Join incognito" xml:space="preserve"> + <source>Join incognito</source> + <target>Приєднуйтесь інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Joining group" xml:space="preserve"> + <source>Joining group</source> + <target>Приєднання до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keep your connections" xml:space="preserve"> + <source>Keep your connections</source> + <target>Зберігайте свої зв'язки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="KeyChain error" xml:space="preserve"> + <source>KeyChain error</source> + <target>помилка KeyChain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Keychain error" xml:space="preserve"> + <source>Keychain error</source> + <target>помилка KeyChain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="LIVE" xml:space="preserve"> + <source>LIVE</source> + <target>НАЖИВО</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Large file!" xml:space="preserve"> + <source>Large file!</source> + <target>Великий файл!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Learn more" xml:space="preserve"> + <source>Learn more</source> + <target>Дізнайтеся більше</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave" xml:space="preserve"> + <source>Leave</source> + <target>Залишити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave group" xml:space="preserve"> + <source>Leave group</source> + <target>Покинути групу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Leave group?" xml:space="preserve"> + <source>Leave group?</source> + <target>Покинути групу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Let's talk in SimpleX Chat" xml:space="preserve"> + <source>Let's talk in SimpleX Chat</source> + <target>Поговоримо в чаті SimpleX</target> + <note>email subject</note> + </trans-unit> + <trans-unit id="Light" xml:space="preserve"> + <source>Light</source> + <target>Світлий</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Limitations" xml:space="preserve"> + <source>Limitations</source> + <target>Обмеження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Live message!" xml:space="preserve"> + <source>Live message!</source> + <target>Живе повідомлення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Live messages" xml:space="preserve"> + <source>Live messages</source> + <target>Живі повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Local name" xml:space="preserve"> + <source>Local name</source> + <target>Місцева назва</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Local profile data only" xml:space="preserve"> + <source>Local profile data only</source> + <target>Тільки локальні дані профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Lock after" xml:space="preserve"> + <source>Lock after</source> + <target>Блокування після</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Lock mode" xml:space="preserve"> + <source>Lock mode</source> + <target>Режим блокування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make a private connection" xml:space="preserve"> + <source>Make a private connection</source> + <target>Створіть приватне з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make one message disappear" xml:space="preserve"> + <source>Make one message disappear</source> + <target>Зробити так, щоб одне повідомлення зникло</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make profile private!" xml:space="preserve"> + <source>Make profile private!</source> + <target>Зробіть профіль приватним!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." xml:space="preserve"> <source>Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@).</source> - <target state="translated">Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@).</target> + <target>Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve" approved="no"> + <trans-unit id="Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." xml:space="preserve"> + <source>Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated.</source> + <target>Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" xml:space="preserve"> + <source>Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*</source> + <target>Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mark deleted for everyone" xml:space="preserve"> + <source>Mark deleted for everyone</source> + <target>Позначити видалено для всіх</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mark read" xml:space="preserve"> + <source>Mark read</source> + <target>Позначити прочитано</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Mark verified" xml:space="preserve"> + <source>Mark verified</source> + <target>Позначити перевірено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Markdown in messages" xml:space="preserve"> + <source>Markdown in messages</source> + <target>Виправлення в повідомленнях</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Max 30 seconds, received instantly." xml:space="preserve"> + <source>Max 30 seconds, received instantly.</source> + <target>Максимум 30 секунд, отримується миттєво.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member" xml:space="preserve"> + <source>Member</source> + <target>Учасник</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". All group members will be notified." xml:space="preserve"> + <source>Member role will be changed to "%@". All group members will be notified.</source> + <target>Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member role will be changed to "%@". The member will receive a new invitation." xml:space="preserve"> + <source>Member role will be changed to "%@". The member will receive a new invitation.</source> + <target>Роль учасника буде змінено на "%@". Учасник отримає нове запрошення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Member will be removed from group - this cannot be undone!" xml:space="preserve"> + <source>Member will be removed from group - this cannot be undone!</source> + <target>Учасник буде видалений з групи - це неможливо скасувати!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message delivery error" xml:space="preserve"> + <source>Message delivery error</source> + <target>Помилка доставки повідомлення</target> + <note>item status text</note> + </trans-unit> + <trans-unit id="Message delivery receipts!" xml:space="preserve"> + <source>Message delivery receipts!</source> + <target>Підтвердження доставки повідомлення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message draft" xml:space="preserve"> + <source>Message draft</source> + <target>Чернетка повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Message reactions" xml:space="preserve"> + <source>Message reactions</source> + <target>Реакції на повідомлення</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Message reactions are prohibited in this chat." xml:space="preserve"> <source>Message reactions are prohibited in this chat.</source> - <target state="translated">Реакції на повідомлення в цьому чаті заборонені.</target> + <target>Реакції на повідомлення в цьому чаті заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve" approved="no"> + <trans-unit id="Message reactions are prohibited in this group." xml:space="preserve"> <source>Message reactions are prohibited in this group.</source> - <target state="translated">Реакції на повідомлення в цій групі заборонені.</target> + <target>Реакції на повідомлення в цій групі заборонені.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Messages & files" xml:space="preserve" approved="no"> + <trans-unit id="Message text" xml:space="preserve"> + <source>Message text</source> + <target>Текст повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages" xml:space="preserve"> + <source>Messages</source> + <target>Повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Messages & files" xml:space="preserve"> <source>Messages & files</source> - <target state="translated">Повідомлення та файли</target> + <target>Повідомлення та файли</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only you can make calls." xml:space="preserve" approved="no"> - <source>Only you can make calls.</source> - <target state="translated">Дзвонити можете тільки ви.</target> + <trans-unit id="Migrating database archive…" xml:space="preserve"> + <source>Migrating database archive…</source> + <target>Перенесення архіву бази даних…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can make calls." xml:space="preserve" approved="no"> - <source>Only your contact can make calls.</source> - <target state="translated">Тільки ваш контакт може здійснювати дзвінки.</target> + <trans-unit id="Migration error:" xml:space="preserve"> + <source>Migration error:</source> + <target>Помилка міграції:</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve" approved="no"> - <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> - <target state="translated">Будь ласка, запам'ятайте або надійно зберігайте його - втрачений пароль неможливо відновити!</target> + <trans-unit id="Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." xml:space="preserve"> + <source>Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat).</source> + <target>Міграція не вдалася. Натисніть **Пропустити** нижче, щоб продовжити використовувати поточну базу даних. Будь ласка, повідомте про проблему розробникам програми через чат або електронну пошту [chat@simplex.chat](mailto:chat@simplex.chat).</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New Passcode" xml:space="preserve" approved="no"> - <source>New Passcode</source> - <target state="translated">Новий пароль</target> + <trans-unit id="Migration is completed" xml:space="preserve"> + <source>Migration is completed</source> + <target>Міграцію завершено</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save welcome message?" xml:space="preserve" approved="no"> - <source>Save welcome message?</source> - <target state="translated">Зберегти вітальне повідомлення?</target> + <trans-unit id="Migrations: %@" xml:space="preserve"> + <source>Migrations: %@</source> + <target>Міграції: %@</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Revoke file" xml:space="preserve" approved="no"> - <source>Revoke file</source> - <target state="translated">Відкликати файл</target> - <note>cancel file action</note> + <trans-unit id="Moderate" xml:space="preserve"> + <source>Moderate</source> + <target>Модерується</target> + <note>chat item action</note> </trans-unit> - <trans-unit id="Save auto-accept settings" xml:space="preserve" approved="no"> - <source>Save auto-accept settings</source> - <target state="translated">Зберегти налаштування автоприйому</target> + <trans-unit id="Moderated at" xml:space="preserve"> + <source>Moderated at</source> + <target>Модерується на</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Self-destruct" xml:space="preserve" approved="no"> - <source>Self-destruct</source> - <target state="translated">Самознищення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Self-destruct passcode" xml:space="preserve" approved="no"> - <source>Self-destruct passcode</source> - <target state="translated">Пароль самознищення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Moderated at: %@" xml:space="preserve" approved="no"> + <trans-unit id="Moderated at: %@" xml:space="preserve"> <source>Moderated at: %@</source> - <target state="translated">Модерується за: %@</target> + <target>Модерується за: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Only you can add message reactions." xml:space="preserve" approved="no"> - <source>Only you can add message reactions.</source> - <target state="translated">Тільки ви можете додавати реакції на повідомлення.</target> + <trans-unit id="More improvements are coming soon!" xml:space="preserve"> + <source>More improvements are coming soon!</source> + <target>Незабаром буде ще більше покращень!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Only your contact can add message reactions." xml:space="preserve" approved="no"> - <source>Only your contact can add message reactions.</source> - <target state="translated">Тільки ваш контакт може додавати реакції на повідомлення.</target> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>Швидше за все, це з'єднання видалено.</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> + <source>Most likely this contact has deleted the connection with you.</source> + <target>Швидше за все, цей контакт видалив зв'язок з вами.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="React..." xml:space="preserve" approved="no"> - <source>React...</source> - <target state="translated">Реагувати...</target> - <note>chat item menu</note> - </trans-unit> - <trans-unit id="Received message" xml:space="preserve" approved="no"> - <source>Received message</source> - <target state="translated">Отримано повідомлення</target> - <note>message info title</note> - </trans-unit> - <trans-unit id="Record updated at" xml:space="preserve" approved="no"> - <source>Record updated at</source> - <target state="translated">Запис оновлено за</target> + <trans-unit id="Multiple chat profiles" xml:space="preserve"> + <source>Multiple chat profiles</source> + <target>Кілька профілів чату</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Record updated at: %@" xml:space="preserve" approved="no"> - <source>Record updated at: %@</source> - <target state="translated">Запис оновлено за: %@</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Revoke" xml:space="preserve" approved="no"> - <source>Revoke</source> - <target state="translated">Відкликати</target> + <trans-unit id="Mute" xml:space="preserve"> + <source>Mute</source> + <target>Вимкнути звук</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Revoke file?" xml:space="preserve" approved="no"> - <source>Revoke file?</source> - <target state="translated">Відкликати файл?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save profile password" xml:space="preserve" approved="no"> - <source>Save profile password</source> - <target state="translated">Зберегти пароль профілю</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Select" xml:space="preserve" approved="no"> - <source>Select</source> - <target state="translated">Виберіть</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve" approved="no"> - <source>Self-destruct passcode enabled!</source> - <target state="translated">Пароль самознищення ввімкнено!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Send disappearing message" xml:space="preserve" approved="no"> - <source>Send disappearing message</source> - <target state="translated">Надіслати зникаюче повідомлення</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Muted when inactive!" xml:space="preserve" approved="no"> + <trans-unit id="Muted when inactive!" xml:space="preserve"> <source>Muted when inactive!</source> - <target state="translated">Вимкнено, коли неактивний!</target> + <target>Вимкнено, коли неактивний!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="No app password" xml:space="preserve" approved="no"> + <trans-unit id="Name" xml:space="preserve"> + <source>Name</source> + <target>Ім'я</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network & servers" xml:space="preserve"> + <source>Network & servers</source> + <target>Мережа та сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network settings" xml:space="preserve"> + <source>Network settings</source> + <target>Налаштування мережі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Network status" xml:space="preserve"> + <source>Network status</source> + <target>Стан мережі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New Passcode" xml:space="preserve"> + <source>New Passcode</source> + <target>Новий пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New contact request" xml:space="preserve"> + <source>New contact request</source> + <target>Новий запит на контакт</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New contact:" xml:space="preserve"> + <source>New contact:</source> + <target>Новий контакт:</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New database archive" xml:space="preserve"> + <source>New database archive</source> + <target>Новий архів бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New display name" xml:space="preserve"> + <source>New display name</source> + <target>Нове ім'я відображення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New in %@" xml:space="preserve"> + <source>New in %@</source> + <target>Нове в %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New member role" xml:space="preserve"> + <source>New member role</source> + <target>Нова роль учасника</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="New message" xml:space="preserve"> + <source>New message</source> + <target>Нове повідомлення</target> + <note>notification</note> + </trans-unit> + <trans-unit id="New passphrase…" xml:space="preserve"> + <source>New passphrase…</source> + <target>Новий пароль…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No" xml:space="preserve"> + <source>No</source> + <target>Ні</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="No app password" xml:space="preserve"> <source>No app password</source> - <target state="translated">Немає пароля програми</target> + <target>Немає пароля програми</target> <note>Authentication unavailable</note> </trans-unit> - <trans-unit id="Off" xml:space="preserve" approved="no"> - <source>Off</source> - <target state="translated">Вимкнено</target> + <trans-unit id="No contacts selected" xml:space="preserve"> + <source>No contacts selected</source> + <target>Не вибрано жодного контакту</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode changed!" xml:space="preserve" approved="no"> - <source>Passcode changed!</source> - <target state="translated">Пароль змінено!</target> + <trans-unit id="No contacts to add" xml:space="preserve"> + <source>No contacts to add</source> + <target>Немає контактів для додавання</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode" xml:space="preserve" approved="no"> - <source>Passcode</source> - <target state="translated">Пароль</target> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>Немає інформації про доставку</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode entry" xml:space="preserve" approved="no"> - <source>Passcode entry</source> - <target state="translated">Введення пароля</target> + <trans-unit id="No device token!" xml:space="preserve"> + <source>No device token!</source> + <target>Токен пристрою відсутній!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode not changed!" xml:space="preserve" approved="no"> - <source>Passcode not changed!</source> - <target state="translated">Пароль не змінено!</target> + <trans-unit id="No filtered chats" xml:space="preserve"> + <source>No filtered chats</source> + <target>Немає фільтрованих чатів</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Passcode set!" xml:space="preserve" approved="no"> - <source>Passcode set!</source> - <target state="translated">Пароль встановлено!</target> + <trans-unit id="No group!" xml:space="preserve"> + <source>Group not found!</source> + <target>Групу не знайдено!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Password to show" xml:space="preserve" approved="no"> - <source>Password to show</source> - <target state="translated">Показати пароль</target> + <trans-unit id="No history" xml:space="preserve"> + <source>No history</source> + <target>Немає історії</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve" approved="no"> - <source>Protect your chat profiles with a password!</source> - <target state="translated">Захистіть свої профілі чату паролем!</target> + <trans-unit id="No permission to record voice message" xml:space="preserve"> + <source>No permission to record voice message</source> + <target>Немає дозволу на запис голосового повідомлення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Save and update group profile" xml:space="preserve" approved="no"> - <source>Save and update group profile</source> - <target state="translated">Збереження та оновлення профілю групи</target> + <trans-unit id="No received or sent files" xml:space="preserve"> + <source>No received or sent files</source> + <target>Немає отриманих або відправлених файлів</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="New display name" xml:space="preserve" approved="no"> - <source>New display name</source> - <target state="translated">Нове ім'я відображення</target> + <trans-unit id="Notifications" xml:space="preserve"> + <source>Notifications</source> + <target>Сповіщення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Prohibit message reactions." xml:space="preserve" approved="no"> - <source>Prohibit message reactions.</source> - <target state="translated">Заборонити реакцію на повідомлення.</target> + <trans-unit id="Notifications are disabled!" xml:space="preserve"> + <source>Notifications are disabled!</source> + <target>Сповіщення вимкнено!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve" approved="no"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> - <target state="translated">Читайте більше в [Посібнику користувача] (https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Receiving file will be stopped." xml:space="preserve" approved="no"> - <source>Receiving file will be stopped.</source> - <target state="translated">Отримання файлу буде зупинено.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save servers?" xml:space="preserve" approved="no"> - <source>Save servers?</source> - <target state="translated">Зберегти сервери?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Save settings?" xml:space="preserve" approved="no"> - <source>Save settings?</source> - <target state="translated">Зберегти налаштування?</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Permanent decryption error" xml:space="preserve" approved="no"> - <source>Permanent decryption error</source> - <target state="translated">Постійна помилка розшифрування</target> - <note>message decrypt error item</note> - </trans-unit> - <trans-unit id="Please report it to the developers." xml:space="preserve" approved="no"> - <source>Please report it to the developers.</source> - <target state="translated">Будь ласка, повідомте про це розробникам.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Polish interface" xml:space="preserve" approved="no"> - <source>Polish interface</source> - <target state="translated">Польський інтерфейс</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Preview" xml:space="preserve" approved="no"> - <source>Preview</source> - <target state="translated">Попередній перегляд</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Profile password" xml:space="preserve" approved="no"> - <source>Profile password</source> - <target state="translated">Пароль до профілю</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit audio/video calls." xml:space="preserve" approved="no"> - <source>Prohibit audio/video calls.</source> - <target state="translated">Заборонити аудіо/відеодзвінки.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve" approved="no"> - <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> - <target state="translated">Читайте більше в [Посібнику користувача] (https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Moderated at" xml:space="preserve" approved="no"> - <source>Moderated at</source> - <target state="translated">Модерується на</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Opening database…" xml:space="preserve" approved="no"> - <source>Opening database…</source> - <target state="translated">Відкриття бази даних…</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Prohibit messages reactions." xml:space="preserve" approved="no"> - <source>Prohibit messages reactions.</source> - <target state="translated">Заборонити реакції на повідомлення.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Received at" xml:space="preserve" approved="no"> - <source>Received at</source> - <target state="translated">Отримано за</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Read more" xml:space="preserve" approved="no"> - <source>Read more</source> - <target state="translated">Читати далі</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Received at: %@" xml:space="preserve" approved="no"> - <source>Received at: %@</source> - <target state="translated">Отримано за: %@</target> - <note>copied message info</note> - </trans-unit> - <trans-unit id="Self-destruct passcode changed!" xml:space="preserve" approved="no"> - <source>Self-destruct passcode changed!</source> - <target state="translated">Пароль самознищення змінено!</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Migrations: %@" xml:space="preserve" approved="no"> - <source>Migrations: %@</source> - <target state="translated">Міграції: %@.</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve" approved="no"> + <trans-unit id="Now admins can: - delete members' messages. - disable members ("observer" role)" xml:space="preserve"> <source>Now admins can: - delete members' messages. - disable members ("observer" role)</source> - <target state="translated">Тепер адміністратори можуть + <target>Тепер адміністратори можуть - видаляти повідомлення користувачів. - відключати користувачів (роль "спостерігач")</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve" approved="no"> - <source>Profile update will be sent to your contacts.</source> - <target state="translated">Оновлення профілю буде надіслано вашим контактам.</target> + <trans-unit id="Off" xml:space="preserve"> + <source>Off</source> + <target>Вимкнено</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve" approved="no"> - <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> - <target state="translated">Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі.</target> + <trans-unit id="Off (Local)" xml:space="preserve"> + <source>Off (Local)</source> + <target>Вимкнено (локально)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve" approved="no"> - <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> - <target state="translated">Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше.</target> + <trans-unit id="Ok" xml:space="preserve"> + <source>Ok</source> + <target>Гаразд</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show:" xml:space="preserve" approved="no"> - <source>Show:</source> - <target state="translated">Показати:</target> + <trans-unit id="Old database" xml:space="preserve"> + <source>Old database</source> + <target>Стара база даних</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Address" xml:space="preserve" approved="no"> - <source>SimpleX Address</source> - <target state="translated">Адреса SimpleX</target> + <trans-unit id="Old database archive" xml:space="preserve"> + <source>Old database archive</source> + <target>Старий архів бази даних</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop file" xml:space="preserve" approved="no"> - <source>Stop file</source> - <target state="translated">Зупинити файл</target> - <note>cancel file action</note> - </trans-unit> - <trans-unit id="There should be at least one user profile." xml:space="preserve" approved="no"> - <source>There should be at least one user profile.</source> - <target state="translated">Повинен бути принаймні один профіль користувача.</target> + <trans-unit id="One-time invitation link" xml:space="preserve"> + <source>One-time invitation link</source> + <target>Посилання на одноразове запрошення</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unfav." xml:space="preserve" approved="no"> - <source>Unfav.</source> - <target state="translated">Нелюб.</target> + <trans-unit id="Onion hosts will be required for connection. Requires enabling VPN." xml:space="preserve"> + <source>Onion hosts will be required for connection. Requires enabling VPN.</source> + <target>Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve" approved="no"> - <source>Server requires authorization to upload, check password</source> - <target state="translated">Сервер вимагає авторизації для завантаження, перевірте пароль</target> - <note>server test error</note> - </trans-unit> - <trans-unit id="SimpleX Lock mode" xml:space="preserve" approved="no"> - <source>SimpleX Lock mode</source> - <target state="translated">Режим SimpleX Lock</target> + <trans-unit id="Onion hosts will be used when available. Requires enabling VPN." xml:space="preserve"> + <source>Onion hosts will be used when available. Requires enabling VPN.</source> + <target>Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Submit" xml:space="preserve" approved="no"> - <source>Submit</source> - <target state="translated">Надіслати</target> + <trans-unit id="Onion hosts will not be used." xml:space="preserve"> + <source>Onion hosts will not be used.</source> + <target>Onion хости не будуть використовуватися.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="System authentication" xml:space="preserve" approved="no"> - <source>System authentication</source> - <target state="translated">Автентифікація системи</target> + <trans-unit id="Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." xml:space="preserve"> + <source>Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**.</source> + <target>Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Tap to activate profile." xml:space="preserve" approved="no"> - <source>Tap to activate profile.</source> - <target state="translated">Натисніть, щоб активувати профіль.</target> + <trans-unit id="Only group owners can change group preferences." xml:space="preserve"> + <source>Only group owners can change group preferences.</source> + <target>Тільки власники груп можуть змінювати налаштування групи.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="There should be at least one visible user profile." xml:space="preserve" approved="no"> - <source>There should be at least one visible user profile.</source> - <target state="translated">Повинен бути принаймні один видимий профіль користувача.</target> + <trans-unit id="Only group owners can enable files and media." xml:space="preserve"> + <source>Only group owners can enable files and media.</source> + <target>Тільки власники груп можуть вмикати файли та медіа.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unhide chat profile" xml:space="preserve" approved="no"> - <source>Unhide chat profile</source> - <target state="translated">Показати профіль чату</target> + <trans-unit id="Only group owners can enable voice messages." xml:space="preserve"> + <source>Only group owners can enable voice messages.</source> + <target>Тільки власники груп можуть вмикати голосові повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unhide profile" xml:space="preserve" approved="no"> - <source>Unhide profile</source> - <target state="translated">Показати профіль</target> + <trans-unit id="Only you can add message reactions." xml:space="preserve"> + <source>Only you can add message reactions.</source> + <target>Тільки ви можете додавати реакції на повідомлення.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unlock app" xml:space="preserve" approved="no"> - <source>Unlock app</source> - <target state="translated">Розблокувати додаток</target> + <trans-unit id="Only you can irreversibly delete messages (your contact can mark them for deletion)." xml:space="preserve"> + <source>Only you can irreversibly delete messages (your contact can mark them for deletion).</source> + <target>Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can make calls." xml:space="preserve"> + <source>Only you can make calls.</source> + <target>Дзвонити можете тільки ви.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can send disappearing messages." xml:space="preserve"> + <source>Only you can send disappearing messages.</source> + <target>Тільки ви можете надсилати зникаючі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only you can send voice messages." xml:space="preserve"> + <source>Only you can send voice messages.</source> + <target>Тільки ви можете надсилати голосові повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can add message reactions." xml:space="preserve"> + <source>Only your contact can add message reactions.</source> + <target>Тільки ваш контакт може додавати реакції на повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can irreversibly delete messages (you can mark them for deletion)." xml:space="preserve"> + <source>Only your contact can irreversibly delete messages (you can mark them for deletion).</source> + <target>Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can make calls." xml:space="preserve"> + <source>Only your contact can make calls.</source> + <target>Тільки ваш контакт може здійснювати дзвінки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can send disappearing messages." xml:space="preserve"> + <source>Only your contact can send disappearing messages.</source> + <target>Тільки ваш контакт може надсилати зникаючі повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Only your contact can send voice messages." xml:space="preserve"> + <source>Only your contact can send voice messages.</source> + <target>Тільки ваш контакт може надсилати голосові повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open Settings" xml:space="preserve"> + <source>Open Settings</source> + <target>Відкрийте Налаштування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open chat" xml:space="preserve"> + <source>Open chat</source> + <target>Відкритий чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Open chat console" xml:space="preserve"> + <source>Open chat console</source> + <target>Відкрийте консоль чату</target> <note>authentication reason</note> </trans-unit> - <trans-unit id="Sent message" xml:space="preserve" approved="no"> - <source>Sent message</source> - <target state="translated">Надіслано повідомлення</target> - <note>message info title</note> + <trans-unit id="Open user profiles" xml:space="preserve"> + <source>Open user profiles</source> + <target>Відкрити профілі користувачів</target> + <note>authentication reason</note> </trans-unit> - <trans-unit id="Set it instead of system authentication." xml:space="preserve" approved="no"> - <source>Set it instead of system authentication.</source> - <target state="translated">Встановіть його замість аутентифікації системи.</target> + <trans-unit id="Open-source protocol and code – anybody can run the servers." xml:space="preserve"> + <source>Open-source protocol and code – anybody can run the servers.</source> + <target>Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share 1-time link" xml:space="preserve" approved="no"> - <source>Share 1-time link</source> - <target state="translated">Поділитися 1-разовим посиланням</target> + <trans-unit id="Opening database…" xml:space="preserve"> + <source>Opening database…</source> + <target>Відкриття бази даних…</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share with contacts" xml:space="preserve" approved="no"> - <source>Share with contacts</source> - <target state="translated">Поділіться з контактами</target> + <trans-unit id="Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." xml:space="preserve"> + <source>Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</source> + <target>Відкриття посилання в браузері може знизити конфіденційність і безпеку з'єднання. Ненадійні посилання SimpleX будуть червоного кольору.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX address" xml:space="preserve" approved="no"> - <source>SimpleX address</source> - <target state="translated">Адреса SimpleX</target> + <trans-unit id="PING count" xml:space="preserve"> + <source>PING count</source> + <target>Кількість PING</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop sharing" xml:space="preserve" approved="no"> - <source>Stop sharing</source> - <target state="translated">Припиніть ділитися</target> + <trans-unit id="PING interval" xml:space="preserve"> + <source>PING interval</source> + <target>Інтервал PING</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop sharing address?" xml:space="preserve" approved="no"> - <source>Stop sharing address?</source> - <target state="translated">Припинити ділитися адресою?</target> + <trans-unit id="Passcode" xml:space="preserve"> + <source>Passcode</source> + <target>Пароль</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The hash of the previous message is different." xml:space="preserve" approved="no"> - <source>The hash of the previous message is different.</source> - <target state="translated">Хеш попереднього повідомлення відрізняється.</target> + <trans-unit id="Passcode changed!" xml:space="preserve"> + <source>Passcode changed!</source> + <target>Пароль змінено!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="This error is permanent for this connection, please re-connect." xml:space="preserve" approved="no"> - <source>This error is permanent for this connection, please re-connect.</source> - <target state="translated">Ця помилка є постійною для цього з'єднання, будь ласка, перепідключіться.</target> + <trans-unit id="Passcode entry" xml:space="preserve"> + <source>Passcode entry</source> + <target>Введення пароля</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unhide" xml:space="preserve" approved="no"> - <source>Unhide</source> - <target state="translated">Показати</target> + <trans-unit id="Passcode not changed!" xml:space="preserve"> + <source>Passcode not changed!</source> + <target>Пароль не змінено!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sent at" xml:space="preserve" approved="no"> - <source>Sent at</source> - <target state="translated">Надіслано за</target> + <trans-unit id="Passcode set!" xml:space="preserve"> + <source>Passcode set!</source> + <target>Пароль встановлено!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sent at: %@" xml:space="preserve" approved="no"> - <source>Sent at: %@</source> - <target state="translated">Надіслано за: %@</target> + <trans-unit id="Password to show" xml:space="preserve"> + <source>Password to show</source> + <target>Показати пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste" xml:space="preserve"> + <source>Paste</source> + <target>Вставити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste image" xml:space="preserve"> + <source>Paste image</source> + <target>Вставити зображення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste received link" xml:space="preserve"> + <source>Paste received link</source> + <target>Вставте отримане посилання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> + <target>Вставте отримане посилання для зв'язку з вашим контактом.</target> + <note>placeholder</note> + </trans-unit> + <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> + <source>People can connect to you only via the links you share.</source> + <target>Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Periodically" xml:space="preserve"> + <source>Periodically</source> + <target>Періодично</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Permanent decryption error" xml:space="preserve"> + <source>Permanent decryption error</source> + <target>Постійна помилка розшифрування</target> + <note>message decrypt error item</note> + </trans-unit> + <trans-unit id="Please ask your contact to enable sending voice messages." xml:space="preserve"> + <source>Please ask your contact to enable sending voice messages.</source> + <target>Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check that you used the correct link or ask your contact to send you another one." xml:space="preserve"> + <source>Please check that you used the correct link or ask your contact to send you another one.</source> + <target>Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check your network connection with %@ and try again." xml:space="preserve"> + <source>Please check your network connection with %@ and try again.</source> + <target>Будь ласка, перевірте підключення до мережі за допомогою %@ і спробуйте ще раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please check yours and your contact preferences." xml:space="preserve"> + <source>Please check yours and your contact preferences.</source> + <target>Будь ласка, перевірте свої та контактні налаштування.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please contact group admin." xml:space="preserve"> + <source>Please contact group admin.</source> + <target>Зверніться до адміністратора групи.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please enter correct current passphrase." xml:space="preserve"> + <source>Please enter correct current passphrase.</source> + <target>Будь ласка, введіть правильний поточний пароль.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please enter the previous password after restoring database backup. This action can not be undone." xml:space="preserve"> + <source>Please enter the previous password after restoring database backup. This action can not be undone.</source> + <target>Будь ласка, введіть попередній пароль після відновлення резервної копії бази даних. Ця дія не може бути скасована.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please remember or store it securely - there is no way to recover a lost passcode!" xml:space="preserve"> + <source>Please remember or store it securely - there is no way to recover a lost passcode!</source> + <target>Будь ласка, запам'ятайте або надійно зберігайте його - втрачений пароль неможливо відновити!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please report it to the developers." xml:space="preserve"> + <source>Please report it to the developers.</source> + <target>Будь ласка, повідомте про це розробникам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please restart the app and migrate the database to enable push notifications." xml:space="preserve"> + <source>Please restart the app and migrate the database to enable push notifications.</source> + <target>Будь ласка, перезапустіть додаток і перенесіть базу даних, щоб увімкнути push-сповіщення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please store passphrase securely, you will NOT be able to access chat if you lose it." xml:space="preserve"> + <source>Please store passphrase securely, you will NOT be able to access chat if you lose it.</source> + <target>Будь ласка, зберігайте пароль надійно, ви НЕ зможете отримати доступ до чату, якщо втратите його.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Please store passphrase securely, you will NOT be able to change it if you lose it." xml:space="preserve"> + <source>Please store passphrase securely, you will NOT be able to change it if you lose it.</source> + <target>Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Polish interface" xml:space="preserve"> + <source>Polish interface</source> + <target>Польський інтерфейс</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Possibly, certificate fingerprint in server address is incorrect" xml:space="preserve"> + <source>Possibly, certificate fingerprint in server address is incorrect</source> + <target>Можливо, в адресі сервера неправильно вказано відбиток сертифіката</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Preserve the last message draft, with attachments." xml:space="preserve"> + <source>Preserve the last message draft, with attachments.</source> + <target>Зберегти чернетку останнього повідомлення з вкладеннями.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preset server" xml:space="preserve"> + <source>Preset server</source> + <target>Попередньо встановлений сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preset server address" xml:space="preserve"> + <source>Preset server address</source> + <target>Попередньо встановлена адреса сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Preview" xml:space="preserve"> + <source>Preview</source> + <target>Попередній перегляд</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy & security" xml:space="preserve"> + <source>Privacy & security</source> + <target>Конфіденційність і безпека</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Privacy redefined" xml:space="preserve"> + <source>Privacy redefined</source> + <target>Конфіденційність переглянута</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Private filenames" xml:space="preserve"> + <source>Private filenames</source> + <target>Приватні імена файлів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile and server connections" xml:space="preserve"> + <source>Profile and server connections</source> + <target>З'єднання профілю та сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile image" xml:space="preserve"> + <source>Profile image</source> + <target>Зображення профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile password" xml:space="preserve"> + <source>Profile password</source> + <target>Пароль до профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Profile update will be sent to your contacts." xml:space="preserve"> + <source>Profile update will be sent to your contacts.</source> + <target>Оновлення профілю буде надіслано вашим контактам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit audio/video calls." xml:space="preserve"> + <source>Prohibit audio/video calls.</source> + <target>Заборонити аудіо/відеодзвінки.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit irreversible message deletion." xml:space="preserve"> + <source>Prohibit irreversible message deletion.</source> + <target>Заборонити незворотне видалення повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit message reactions." xml:space="preserve"> + <source>Prohibit message reactions.</source> + <target>Заборонити реакцію на повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit messages reactions." xml:space="preserve"> + <source>Prohibit messages reactions.</source> + <target>Заборонити реакції на повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending direct messages to members." xml:space="preserve"> + <source>Prohibit sending direct messages to members.</source> + <target>Заборонити надсилати прямі повідомлення учасникам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending disappearing messages." xml:space="preserve"> + <source>Prohibit sending disappearing messages.</source> + <target>Заборонити надсилання зникаючих повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending files and media." xml:space="preserve"> + <source>Prohibit sending files and media.</source> + <target>Заборонити надсилання файлів і медіа.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Prohibit sending voice messages." xml:space="preserve"> + <source>Prohibit sending voice messages.</source> + <target>Заборонити надсилання голосових повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect app screen" xml:space="preserve"> + <source>Protect app screen</source> + <target>Захистіть екран програми</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protect your chat profiles with a password!" xml:space="preserve"> + <source>Protect your chat profiles with a password!</source> + <target>Захистіть свої профілі чату паролем!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protocol timeout" xml:space="preserve"> + <source>Protocol timeout</source> + <target>Тайм-аут протоколу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Protocol timeout per KB" xml:space="preserve"> + <source>Protocol timeout per KB</source> + <target>Тайм-аут протоколу на КБ</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Push notifications" xml:space="preserve"> + <source>Push notifications</source> + <target>Push-повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Rate the app" xml:space="preserve"> + <source>Rate the app</source> + <target>Оцініть додаток</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="React…" xml:space="preserve"> + <source>React…</source> + <target>Реагуй…</target> + <note>chat item menu</note> + </trans-unit> + <trans-unit id="Read" xml:space="preserve"> + <source>Read</source> + <target>Читати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more" xml:space="preserve"> + <source>Read more</source> + <target>Читати далі</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</source> + <target>Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." xml:space="preserve"> + <source>Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</source> + <target>Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in our GitHub repository." xml:space="preserve"> + <source>Read more in our GitHub repository.</source> + <target>Читайте більше в нашому репозиторії на GitHub.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." xml:space="preserve"> + <source>Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme).</source> + <target>Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>Підтвердження виключені</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at" xml:space="preserve"> + <source>Received at</source> + <target>Отримано за</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Received at: %@" xml:space="preserve"> + <source>Received at: %@</source> + <target>Отримано за: %@</target> <note>copied message info</note> </trans-unit> - <trans-unit id="Set the message shown to new members!" xml:space="preserve" approved="no"> + <trans-unit id="Received file event" xml:space="preserve"> + <source>Received file event</source> + <target>Подія отримання файлу</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Received message" xml:space="preserve"> + <source>Received message</source> + <target>Отримано повідомлення</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Receiving address will be changed to a different server. Address change will complete after sender comes online." xml:space="preserve"> + <source>Receiving address will be changed to a different server. Address change will complete after sender comes online.</source> + <target>Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving file will be stopped." xml:space="preserve"> + <source>Receiving file will be stopped.</source> + <target>Отримання файлу буде зупинено.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Receiving via" xml:space="preserve"> + <source>Receiving via</source> + <target>Отримання через</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Recipients see updates as you type them." xml:space="preserve"> + <source>Recipients see updates as you type them.</source> + <target>Одержувачі бачать оновлення, коли ви їх вводите.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> + <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reconnect servers?" xml:space="preserve"> + <source>Reconnect servers?</source> + <target>Перепідключити сервери?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at" xml:space="preserve"> + <source>Record updated at</source> + <target>Запис оновлено за</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Record updated at: %@" xml:space="preserve"> + <source>Record updated at: %@</source> + <target>Запис оновлено за: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Reduced battery usage" xml:space="preserve"> + <source>Reduced battery usage</source> + <target>Зменшення використання акумулятора</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reject" xml:space="preserve"> + <source>Reject</source> + <target>Відхилити</target> + <note>reject incoming call via notification</note> + </trans-unit> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> + <target>Відхилити (відправника НЕ повідомлено)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reject contact request" xml:space="preserve"> + <source>Reject contact request</source> + <target>Відхилити запит на контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Relay server is only used if necessary. Another party can observe your IP address." xml:space="preserve"> + <source>Relay server is only used if necessary. Another party can observe your IP address.</source> + <target>Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Relay server protects your IP address, but it can observe the duration of the call." xml:space="preserve"> + <source>Relay server protects your IP address, but it can observe the duration of the call.</source> + <target>Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove" xml:space="preserve"> + <source>Remove</source> + <target>Видалити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove member" xml:space="preserve"> + <source>Remove member</source> + <target>Видалити учасника</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove member?" xml:space="preserve"> + <source>Remove member?</source> + <target>Видалити учасника?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Remove passphrase from keychain?" xml:space="preserve"> + <source>Remove passphrase from keychain?</source> + <target>Видалити парольну фразу з брелока?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate" xml:space="preserve"> + <source>Renegotiate</source> + <target>Переузгодьте</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption" xml:space="preserve"> + <source>Renegotiate encryption</source> + <target>Переузгодьте шифрування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Renegotiate encryption?" xml:space="preserve"> + <source>Renegotiate encryption?</source> + <target>Переузгодьте шифрування?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reply" xml:space="preserve"> + <source>Reply</source> + <target>Відповісти</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Required" xml:space="preserve"> + <source>Required</source> + <target>Потрібно</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset" xml:space="preserve"> + <source>Reset</source> + <target>Перезавантаження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset colors" xml:space="preserve"> + <source>Reset colors</source> + <target>Скинути кольори</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reset to defaults" xml:space="preserve"> + <source>Reset to defaults</source> + <target>Відновити налаштування за замовчуванням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restart the app to create a new chat profile" xml:space="preserve"> + <source>Restart the app to create a new chat profile</source> + <target>Перезапустіть програму, щоб створити новий профіль чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restart the app to use imported chat database" xml:space="preserve"> + <source>Restart the app to use imported chat database</source> + <target>Перезапустіть програму, щоб використовувати імпортовану базу даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore" xml:space="preserve"> + <source>Restore</source> + <target>Відновити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore database backup" xml:space="preserve"> + <source>Restore database backup</source> + <target>Відновлення резервної копії бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore database backup?" xml:space="preserve"> + <source>Restore database backup?</source> + <target>Відновити резервну копію бази даних?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Restore database error" xml:space="preserve"> + <source>Restore database error</source> + <target>Відновлення помилки бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Reveal" xml:space="preserve"> + <source>Reveal</source> + <target>Показувати</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Revert" xml:space="preserve"> + <source>Revert</source> + <target>Повернутися</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Revoke" xml:space="preserve"> + <source>Revoke</source> + <target>Відкликати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Revoke file" xml:space="preserve"> + <source>Revoke file</source> + <target>Відкликати файл</target> + <note>cancel file action</note> + </trans-unit> + <trans-unit id="Revoke file?" xml:space="preserve"> + <source>Revoke file?</source> + <target>Відкликати файл?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Role" xml:space="preserve"> + <source>Role</source> + <target>Роль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Run chat" xml:space="preserve"> + <source>Run chat</source> + <target>Запустити чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SMP servers" xml:space="preserve"> + <source>SMP servers</source> + <target>Сервери SMP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save" xml:space="preserve"> + <source>Save</source> + <target>Зберегти</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Save (and notify contacts)" xml:space="preserve"> + <source>Save (and notify contacts)</source> + <target>Зберегти (і повідомити контактам)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and notify contact" xml:space="preserve"> + <source>Save and notify contact</source> + <target>Зберегти та повідомити контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and notify group members" xml:space="preserve"> + <source>Save and notify group members</source> + <target>Зберегти та повідомити учасників групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save and update group profile" xml:space="preserve"> + <source>Save and update group profile</source> + <target>Збереження та оновлення профілю групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save archive" xml:space="preserve"> + <source>Save archive</source> + <target>Зберегти архів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save auto-accept settings" xml:space="preserve"> + <source>Save auto-accept settings</source> + <target>Зберегти налаштування автоприйому</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save group profile" xml:space="preserve"> + <source>Save group profile</source> + <target>Зберегти профіль групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save passphrase and open chat" xml:space="preserve"> + <source>Save passphrase and open chat</source> + <target>Збережіть пароль і відкрийте чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save passphrase in Keychain" xml:space="preserve"> + <source>Save passphrase in Keychain</source> + <target>Збережіть парольну фразу в Keychain</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save preferences?" xml:space="preserve"> + <source>Save preferences?</source> + <target>Зберегти налаштування?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save profile password" xml:space="preserve"> + <source>Save profile password</source> + <target>Зберегти пароль профілю</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save servers" xml:space="preserve"> + <source>Save servers</source> + <target>Зберегти сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save servers?" xml:space="preserve"> + <source>Save servers?</source> + <target>Зберегти сервери?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save settings?" xml:space="preserve"> + <source>Save settings?</source> + <target>Зберегти налаштування?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Save welcome message?" xml:space="preserve"> + <source>Save welcome message?</source> + <target>Зберегти вітальне повідомлення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Saved WebRTC ICE servers will be removed" xml:space="preserve"> + <source>Saved WebRTC ICE servers will be removed</source> + <target>Збережені сервери WebRTC ICE буде видалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan QR code" xml:space="preserve"> + <source>Scan QR code</source> + <target>Відскануйте QR-код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan code" xml:space="preserve"> + <source>Scan code</source> + <target>Сканувати код</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan security code from your contact's app." xml:space="preserve"> + <source>Scan security code from your contact's app.</source> + <target>Відскануйте код безпеки з додатку вашого контакту.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Scan server QR code" xml:space="preserve"> + <source>Scan server QR code</source> + <target>Відскануйте QR-код сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Search" xml:space="preserve"> + <source>Search</source> + <target>Пошук</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Secure queue" xml:space="preserve"> + <source>Secure queue</source> + <target>Безпечна черга</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Security assessment" xml:space="preserve"> + <source>Security assessment</source> + <target>Оцінка безпеки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Security code" xml:space="preserve"> + <source>Security code</source> + <target>Код безпеки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Select" xml:space="preserve"> + <source>Select</source> + <target>Виберіть</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct" xml:space="preserve"> + <source>Self-destruct</source> + <target>Самознищення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode" xml:space="preserve"> + <source>Self-destruct passcode</source> + <target>Пароль самознищення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode changed!" xml:space="preserve"> + <source>Self-destruct passcode changed!</source> + <target>Пароль самознищення змінено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Self-destruct passcode enabled!" xml:space="preserve"> + <source>Self-destruct passcode enabled!</source> + <target>Пароль самознищення ввімкнено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send" xml:space="preserve"> + <source>Send</source> + <target>Надіслати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send a live message - it will update for the recipient(s) as you type it" xml:space="preserve"> + <source>Send a live message - it will update for the recipient(s) as you type it</source> + <target>Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send delivery receipts to" xml:space="preserve"> + <source>Send delivery receipts to</source> + <target>Надсилання звітів про доставку</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send direct message" xml:space="preserve"> + <source>Send direct message</source> + <target>Надішліть пряме повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send disappearing message" xml:space="preserve"> + <source>Send disappearing message</source> + <target>Надіслати зникаюче повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send link previews" xml:space="preserve"> + <source>Send link previews</source> + <target>Надіслати попередній перегляд за посиланням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send live message" xml:space="preserve"> + <source>Send live message</source> + <target>Надіслати живе повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send notifications" xml:space="preserve"> + <source>Send notifications</source> + <target>Надсилати сповіщення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send notifications:" xml:space="preserve"> + <source>Send notifications:</source> + <target>Надсилати сповіщення:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send questions and ideas" xml:space="preserve"> + <source>Send questions and ideas</source> + <target>Надсилайте запитання та ідеї</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send receipts" xml:space="preserve"> + <source>Send receipts</source> + <target>Надіслати підтвердження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> + <source>Send them from gallery or custom keyboards.</source> + <target>Надсилайте їх із галереї чи власних клавіатур.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sender cancelled file transfer." xml:space="preserve"> + <source>Sender cancelled file transfer.</source> + <target>Відправник скасував передачу файлу.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sender may have deleted the connection request." xml:space="preserve"> + <source>Sender may have deleted the connection request.</source> + <target>Можливо, відправник видалив запит на підключення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>Надсилання підтверджень доставки буде ввімкнено для всіх контактів у всіх видимих профілях чату.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> + <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>Надсилання підтверджень доставки буде ввімкнено для всіх контактів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending file will be stopped." xml:space="preserve"> + <source>Sending file will be stopped.</source> + <target>Надсилання файлу буде зупинено.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is disabled for %lld contacts</source> + <target>Надсилання підтвердження вимкнено для контактів %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>Відправлення підтверджень вимкнено для груп %lld</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> + <source>Sending receipts is enabled for %lld contacts</source> + <target>Для контактів %lld увімкнено надсилання підтвердження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>Для груп %lld увімкнено надсилання підтвердження</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending via" xml:space="preserve"> + <source>Sending via</source> + <target>Надсилання через</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent at" xml:space="preserve"> + <source>Sent at</source> + <target>Надіслано за</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sent at: %@" xml:space="preserve"> + <source>Sent at: %@</source> + <target>Надіслано за: %@</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="Sent file event" xml:space="preserve"> + <source>Sent file event</source> + <target>Подія надісланого файлу</target> + <note>notification</note> + </trans-unit> + <trans-unit id="Sent message" xml:space="preserve"> + <source>Sent message</source> + <target>Надіслано повідомлення</target> + <note>message info title</note> + </trans-unit> + <trans-unit id="Sent messages will be deleted after set time." xml:space="preserve"> + <source>Sent messages will be deleted after set time.</source> + <target>Надіслані повідомлення будуть видалені через встановлений час.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Server requires authorization to create queues, check password" xml:space="preserve"> + <source>Server requires authorization to create queues, check password</source> + <target>Сервер вимагає авторизації для створення черг, перевірте пароль</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Server requires authorization to upload, check password" xml:space="preserve"> + <source>Server requires authorization to upload, check password</source> + <target>Сервер вимагає авторизації для завантаження, перевірте пароль</target> + <note>server test error</note> + </trans-unit> + <trans-unit id="Server test failed!" xml:space="preserve"> + <source>Server test failed!</source> + <target>Тест сервера завершився невдало!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Servers" xml:space="preserve"> + <source>Servers</source> + <target>Сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set 1 day" xml:space="preserve"> + <source>Set 1 day</source> + <target>Встановити 1 день</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set contact name…" xml:space="preserve"> + <source>Set contact name…</source> + <target>Встановити ім'я контакту…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set group preferences" xml:space="preserve"> + <source>Set group preferences</source> + <target>Встановіть налаштування групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set it instead of system authentication." xml:space="preserve"> + <source>Set it instead of system authentication.</source> + <target>Встановіть його замість аутентифікації системи.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passcode" xml:space="preserve"> + <source>Set passcode</source> + <target>Встановити пароль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set passphrase to export" xml:space="preserve"> + <source>Set passphrase to export</source> + <target>Встановити ключову фразу для експорту</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Set the message shown to new members!" xml:space="preserve"> <source>Set the message shown to new members!</source> - <target state="translated">Налаштуйте повідомлення, яке показуватиметься новим користувачам!</target> + <target>Налаштуйте повідомлення, яке показуватиметься новим користувачам!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Show developer options" xml:space="preserve" approved="no"> + <trans-unit id="Set timeouts for proxy/VPN" xml:space="preserve"> + <source>Set timeouts for proxy/VPN</source> + <target>Встановлення таймаутів для проксі/VPN</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Settings" xml:space="preserve"> + <source>Settings</source> + <target>Налаштування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share" xml:space="preserve"> + <source>Share</source> + <target>Поділіться</target> + <note>chat item action</note> + </trans-unit> + <trans-unit id="Share 1-time link" xml:space="preserve"> + <source>Share 1-time link</source> + <target>Поділитися 1-разовим посиланням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address" xml:space="preserve"> + <source>Share address</source> + <target>Поділитися адресою</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share address with contacts?" xml:space="preserve"> + <source>Share address with contacts?</source> + <target>Поділіться адресою з контактами?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share link" xml:space="preserve"> + <source>Share link</source> + <target>Поділіться посиланням</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share one-time invitation link" xml:space="preserve"> + <source>Share one-time invitation link</source> + <target>Поділіться посиланням на одноразове запрошення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Share with contacts" xml:space="preserve"> + <source>Share with contacts</source> + <target>Поділіться з контактами</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show calls in phone history" xml:space="preserve"> + <source>Show calls in phone history</source> + <target>Показувати дзвінки в історії дзвінків</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show developer options" xml:space="preserve"> <source>Show developer options</source> - <target state="translated">Показати опції розробника</target> + <target>Показати опції розробника</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve" approved="no"> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>Показати останні повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show preview" xml:space="preserve"> + <source>Show preview</source> + <target>Показати попередній перегляд</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Show:" xml:space="preserve"> + <source>Show:</source> + <target>Показати:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Address" xml:space="preserve"> + <source>SimpleX Address</source> + <target>Адреса SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Chat security was audited by Trail of Bits." xml:space="preserve"> + <source>SimpleX Chat security was audited by Trail of Bits.</source> + <target>Безпека SimpleX Chat була перевірена компанією Trail of Bits.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock" xml:space="preserve"> + <source>SimpleX Lock</source> + <target>SimpleX Lock</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock mode" xml:space="preserve"> + <source>SimpleX Lock mode</source> + <target>Режим SimpleX Lock</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve"> + <source>SimpleX Lock not enabled!</source> + <target>SimpleX Lock не ввімкнено!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX Lock turned on" xml:space="preserve"> + <source>SimpleX Lock turned on</source> + <target>SimpleX Lock увімкнено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX address" xml:space="preserve"> + <source>SimpleX address</source> + <target>Адреса SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX contact address" xml:space="preserve"> + <source>SimpleX contact address</source> + <target>Контактна адреса SimpleX</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="SimpleX encrypted message or connection event" xml:space="preserve"> + <source>SimpleX encrypted message or connection event</source> + <target>Зашифроване повідомлення SimpleX або подія підключення</target> + <note>notification</note> + </trans-unit> + <trans-unit id="SimpleX group link" xml:space="preserve"> + <source>SimpleX group link</source> + <target>Посилання на групу SimpleX</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="SimpleX links" xml:space="preserve"> + <source>SimpleX links</source> + <target>Посилання SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="SimpleX one-time invitation" xml:space="preserve"> + <source>SimpleX one-time invitation</source> + <target>Одноразове запрошення SimpleX</target> + <note>simplex link type</note> + </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Skip" xml:space="preserve"> + <source>Skip</source> + <target>Пропустити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Skipped messages" xml:space="preserve"> + <source>Skipped messages</source> + <target>Пропущені повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>Невеликі групи (максимум 20 осіб)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> + <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> + <target>Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Somebody" xml:space="preserve"> + <source>Somebody</source> + <target>Хтось</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="Start a new chat" xml:space="preserve"> + <source>Start a new chat</source> + <target>Почніть новий чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Start chat" xml:space="preserve"> + <source>Start chat</source> + <target>Почати чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Start migration" xml:space="preserve"> + <source>Start migration</source> + <target>Почати міграцію</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop" xml:space="preserve"> + <source>Stop</source> + <target>Зупинити</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop SimpleX" xml:space="preserve"> + <source>Stop SimpleX</source> + <target>Зупинити SimpleX</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Stop chat to enable database actions" xml:space="preserve"> + <source>Stop chat to enable database actions</source> + <target>Зупиніть чат, щоб увімкнути дії з базою даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." xml:space="preserve"> + <source>Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped.</source> + <target>Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop chat?" xml:space="preserve"> + <source>Stop chat?</source> + <target>Зупинити чат?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop file" xml:space="preserve"> + <source>Stop file</source> + <target>Зупинити файл</target> + <note>cancel file action</note> + </trans-unit> + <trans-unit id="Stop receiving file?" xml:space="preserve"> + <source>Stop receiving file?</source> + <target>Припинити отримання файлу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sending file?" xml:space="preserve"> + <source>Stop sending file?</source> + <target>Припинити надсилання файлу?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sharing" xml:space="preserve"> + <source>Stop sharing</source> + <target>Припиніть ділитися</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Stop sharing address?" xml:space="preserve"> + <source>Stop sharing address?</source> + <target>Припинити ділитися адресою?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Submit" xml:space="preserve"> + <source>Submit</source> + <target>Надіслати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Support SimpleX Chat" xml:space="preserve"> + <source>Support SimpleX Chat</source> + <target>Підтримка чату SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="System" xml:space="preserve"> + <source>System</source> + <target>Система</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="System authentication" xml:space="preserve"> + <source>System authentication</source> + <target>Автентифікація системи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP connection timeout" xml:space="preserve"> + <source>TCP connection timeout</source> + <target>Тайм-аут TCP-з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP_KEEPCNT" xml:space="preserve"> + <source>TCP_KEEPCNT</source> + <target>TCP_KEEPCNT</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP_KEEPIDLE" xml:space="preserve"> + <source>TCP_KEEPIDLE</source> + <target>TCP_KEEPIDLE</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="TCP_KEEPINTVL" xml:space="preserve"> + <source>TCP_KEEPINTVL</source> + <target>TCP_KEEPINTVL</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Take picture" xml:space="preserve"> + <source>Take picture</source> + <target>Сфотографуйте</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap button " xml:space="preserve"> + <source>Tap button </source> + <target>Натисніть кнопку </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to activate profile." xml:space="preserve"> + <source>Tap to activate profile.</source> + <target>Натисніть, щоб активувати профіль.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to join" xml:space="preserve"> + <source>Tap to join</source> + <target>Натисніть, щоб приєднатися</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to join incognito" xml:space="preserve"> + <source>Tap to join incognito</source> + <target>Натисніть, щоб приєднатися інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tap to start a new chat" xml:space="preserve"> + <source>Tap to start a new chat</source> + <target>Натисніть, щоб почати новий чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Test failed at step %@." xml:space="preserve"> + <source>Test failed at step %@.</source> + <target>Тест завершився невдало на кроці %@.</target> + <note>server test failure</note> + </trans-unit> + <trans-unit id="Test server" xml:space="preserve"> + <source>Test server</source> + <target>Тестовий сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Test servers" xml:space="preserve"> + <source>Test servers</source> + <target>Тестові сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Tests failed!" xml:space="preserve"> + <source>Tests failed!</source> + <target>Тести не пройшли!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Thank you for installing SimpleX Chat!" xml:space="preserve"> + <source>Thank you for installing SimpleX Chat!</source> + <target>Дякуємо, що встановили SimpleX Chat!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <target>Дякуємо користувачам - [внесок через Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Thanks to the users – contribute via Weblate!" xml:space="preserve"> + <source>Thanks to the users – contribute via Weblate!</source> + <target>Дякуємо користувачам - зробіть свій внесок через Weblate!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The 1st platform without any user identifiers – private by design." xml:space="preserve"> + <source>The 1st platform without any user identifiers – private by design.</source> + <target>Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised." xml:space="preserve"> <source>The ID of the next message is incorrect (less or equal to the previous). It can happen because of some bug or when the connection is compromised.</source> - <target state="translated">Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому). + <target>Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому). Це може статися через помилку або коли з'єднання скомпрометовано.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Sending file will be stopped." xml:space="preserve" approved="no"> - <source>Sending file will be stopped.</source> - <target state="translated">Надсилання файлу буде зупинено.</target> + <trans-unit id="The app can notify you when you receive messages or contact requests - please open settings to enable." xml:space="preserve"> + <source>The app can notify you when you receive messages or contact requests - please open settings to enable.</source> + <target>Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Set passcode" xml:space="preserve" approved="no"> - <source>Set passcode</source> - <target state="translated">Встановити пароль</target> + <trans-unit id="The attempt to change database passphrase was not completed." xml:space="preserve"> + <source>The attempt to change database passphrase was not completed.</source> + <target>Спроба змінити пароль до бази даних не була завершена.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share address with contacts?" xml:space="preserve" approved="no"> - <source>Share address with contacts?</source> - <target state="translated">Поділіться адресою з контактами?</target> + <trans-unit id="The connection you accepted will be cancelled!" xml:space="preserve"> + <source>The connection you accepted will be cancelled!</source> + <target>Прийняте вами з'єднання буде скасовано!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Share address" xml:space="preserve" approved="no"> - <source>Share address</source> - <target state="translated">Поділитися адресою</target> + <trans-unit id="The contact you shared this link with will NOT be able to connect!" xml:space="preserve"> + <source>The contact you shared this link with will NOT be able to connect!</source> + <target>Контакт, з яким ви поділилися цим посиланням, НЕ зможе підключитися!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="SimpleX Lock not enabled!" xml:space="preserve" approved="no"> - <source>SimpleX Lock not enabled!</source> - <target state="translated">SimpleX Lock не ввімкнено!</target> + <trans-unit id="The created archive is available via app Settings / Database / Old database archive." xml:space="preserve"> + <source>The created archive is available via app Settings / Database / Old database archive.</source> + <target>Створений архів доступний через Налаштування програми / База даних / Старий архів бази даних.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop receiving file?" xml:space="preserve" approved="no"> - <source>Stop receiving file?</source> - <target state="translated">Припинити отримання файлу?</target> + <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> + <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Stop sending file?" xml:space="preserve" approved="no"> - <source>Stop sending file?</source> - <target state="translated">Припинити надсилання файлу?</target> + <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> + <source>The group is fully decentralized – it is visible only to the members.</source> + <target>Група повністю децентралізована - її бачать лише учасники.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve" approved="no"> + <trans-unit id="The hash of the previous message is different." xml:space="preserve"> + <source>The hash of the previous message is different.</source> + <target>Хеш попереднього повідомлення відрізняється.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The message will be deleted for all members." xml:space="preserve"> + <source>The message will be deleted for all members.</source> + <target>Повідомлення буде видалено для всіх учасників.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The message will be marked as moderated for all members." xml:space="preserve"> + <source>The message will be marked as moderated for all members.</source> + <target>Повідомлення буде позначено як модероване для всіх учасників.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The next generation of private messaging" xml:space="preserve"> + <source>The next generation of private messaging</source> + <target>Наступне покоління приватних повідомлень</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The old database was not removed during the migration, it can be deleted." xml:space="preserve"> + <source>The old database was not removed during the migration, it can be deleted.</source> + <target>Стара база даних не була видалена під час міграції, її можна видалити.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The profile is only shared with your contacts." xml:space="preserve"> + <source>The profile is only shared with your contacts.</source> + <target>Профіль доступний лише вашим контактам.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> + <source>The second tick we missed! ✅</source> + <target>Другу галочку ми пропустили! ✅</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The sender will NOT be notified" xml:space="preserve"> + <source>The sender will NOT be notified</source> + <target>Відправник НЕ буде повідомлений</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="The servers for new connections of your current chat profile **%@**." xml:space="preserve"> + <source>The servers for new connections of your current chat profile **%@**.</source> + <target>Сервери для нових підключень вашого поточного профілю чату **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Theme" xml:space="preserve"> + <source>Theme</source> + <target>Тема</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="There should be at least one user profile." xml:space="preserve"> + <source>There should be at least one user profile.</source> + <target>Повинен бути принаймні один профіль користувача.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="There should be at least one visible user profile." xml:space="preserve"> + <source>There should be at least one visible user profile.</source> + <target>Повинен бути принаймні один видимий профіль користувача.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> + <source>These settings are for your current profile **%@**.</source> + <target>Ці налаштування стосуються вашого поточного профілю **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>Їх можна перевизначити в налаштуваннях контактів і груп.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> + <source>This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain.</source> + <target>Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." xml:space="preserve"> + <source>This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes.</source> + <target>Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." xml:space="preserve"> + <source>This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.</source> + <target>Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>У цій групі більше %lld учасників, підтвердження доставки не надсилаються.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This group no longer exists." xml:space="preserve"> + <source>This group no longer exists.</source> + <target>Цієї групи більше не існує.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="This setting applies to messages in your current chat profile **%@**." xml:space="preserve"> + <source>This setting applies to messages in your current chat profile **%@**.</source> + <target>Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To ask any questions and to receive updates:" xml:space="preserve"> + <source>To ask any questions and to receive updates:</source> + <target>Задати будь-які питання та отримувати новини:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To connect, your contact can scan QR code or use the link in the app." xml:space="preserve"> <source>To connect, your contact can scan QR code or use the link in the app.</source> - <target state="translated">Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку.</target> + <target>Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve" approved="no"> + <trans-unit id="To make a new connection" xml:space="preserve"> + <source>To make a new connection</source> + <target>Щоб створити нове з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." xml:space="preserve"> + <source>To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts.</source> + <target>Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect timezone, image/voice files use UTC." xml:space="preserve"> + <source>To protect timezone, image/voice files use UTC.</source> + <target>Для захисту часового поясу у файлах зображень/голосу використовується UTC.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled." xml:space="preserve"> + <source>To protect your information, turn on SimpleX Lock. +You will be prompted to complete authentication before this feature is enabled.</source> + <target>Щоб захистити вашу інформацію, увімкніть SimpleX Lock. +Перед увімкненням цієї функції вам буде запропоновано пройти автентифікацію.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To record voice message please grant permission to use Microphone." xml:space="preserve"> + <source>To record voice message please grant permission to use Microphone.</source> + <target>Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." xml:space="preserve"> <source>To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.</source> - <target state="translated">Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**.</target> + <target>Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**.</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Unit" xml:space="preserve" approved="no"> + <trans-unit id="To support instant push notifications the chat database has to be migrated." xml:space="preserve"> + <source>To support instant push notifications the chat database has to be migrated.</source> + <target>Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="To verify end-to-end encryption with your contact compare (or scan) the code on your devices." xml:space="preserve"> + <source>To verify end-to-end encryption with your contact compare (or scan) the code on your devices.</source> + <target>Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Transport isolation" xml:space="preserve"> + <source>Transport isolation</source> + <target>Транспортна ізоляція</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Trying to connect to the server used to receive messages from this contact (error: %@)." xml:space="preserve"> + <source>Trying to connect to the server used to receive messages from this contact (error: %@).</source> + <target>Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Trying to connect to the server used to receive messages from this contact." xml:space="preserve"> + <source>Trying to connect to the server used to receive messages from this contact.</source> + <target>Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turn off" xml:space="preserve"> + <source>Turn off</source> + <target>Вимкнути</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turn off notifications?" xml:space="preserve"> + <source>Turn off notifications?</source> + <target>Вимкнути сповіщення?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Turn on" xml:space="preserve"> + <source>Turn on</source> + <target>Ввімкнути</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unable to record voice message" xml:space="preserve"> + <source>Unable to record voice message</source> + <target>Не вдається записати голосове повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unexpected error: %@" xml:space="preserve"> + <source>Unexpected error: %@</source> + <target>Неочікувана помилка: %@</target> + <note>item status description</note> + </trans-unit> + <trans-unit id="Unexpected migration state" xml:space="preserve"> + <source>Unexpected migration state</source> + <target>Неочікуваний стан міграції</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unfav." xml:space="preserve"> + <source>Unfav.</source> + <target>Нелюб.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide" xml:space="preserve"> + <source>Unhide</source> + <target>Показати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide chat profile" xml:space="preserve"> + <source>Unhide chat profile</source> + <target>Показати профіль чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unhide profile" xml:space="preserve"> + <source>Unhide profile</source> + <target>Показати профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unit" xml:space="preserve"> <source>Unit</source> - <target state="translated">Одиниця</target> + <target>Одиниця</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown caller" xml:space="preserve"> + <source>Unknown caller</source> + <target>Невідомий абонент</target> + <note>callkit banner</note> + </trans-unit> + <trans-unit id="Unknown database error: %@" xml:space="preserve"> + <source>Unknown database error: %@</source> + <target>Невідома помилка бази даних: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unknown error" xml:space="preserve"> + <source>Unknown error</source> + <target>Невідома помилка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." xml:space="preserve"> + <source>Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.</source> + <target>Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим "Не турбувати", щоб уникнути переривань.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection." xml:space="preserve"> + <source>Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. +To connect, please ask your contact to create another connection link and check that you have a stable network connection.</source> + <target>Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це. +Щоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlock" xml:space="preserve"> + <source>Unlock</source> + <target>Розблокувати</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unlock app" xml:space="preserve"> + <source>Unlock app</source> + <target>Розблокувати додаток</target> + <note>authentication reason</note> + </trans-unit> + <trans-unit id="Unmute" xml:space="preserve"> + <source>Unmute</source> + <target>Увімкнути звук</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Unread" xml:space="preserve"> + <source>Unread</source> + <target>Непрочитане</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update" xml:space="preserve"> + <source>Update</source> + <target>Оновлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update .onion hosts setting?" xml:space="preserve"> + <source>Update .onion hosts setting?</source> + <target>Оновити налаштування хостів .onion?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update database passphrase" xml:space="preserve"> + <source>Update database passphrase</source> + <target>Оновити парольну фразу бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update network settings?" xml:space="preserve"> + <source>Update network settings?</source> + <target>Оновити налаштування мережі?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Update transport isolation mode?" xml:space="preserve"> + <source>Update transport isolation mode?</source> + <target>Оновити режим транспортної ізоляції?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updating settings will re-connect the client to all servers." xml:space="preserve"> + <source>Updating settings will re-connect the client to all servers.</source> + <target>Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Updating this setting will re-connect the client to all servers." xml:space="preserve"> + <source>Updating this setting will re-connect the client to all servers.</source> + <target>Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upgrade and open chat" xml:space="preserve"> + <source>Upgrade and open chat</source> + <target>Оновлення та відкритий чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Upload file" xml:space="preserve"> + <source>Upload file</source> + <target>Завантажити файл</target> + <note>server test step</note> + </trans-unit> + <trans-unit id="Use .onion hosts" xml:space="preserve"> + <source>Use .onion hosts</source> + <target>Використовуйте хости .onion</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use SimpleX Chat servers?" xml:space="preserve"> + <source>Use SimpleX Chat servers?</source> + <target>Використовувати сервери SimpleX Chat?</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use chat" xml:space="preserve"> + <source>Use chat</source> + <target>Використовуйте чат</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>Використовувати поточний профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use for new connections" xml:space="preserve"> + <source>Use for new connections</source> + <target>Використовуйте для нових з'єднань</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use iOS call interface" xml:space="preserve"> + <source>Use iOS call interface</source> + <target>Використовуйте інтерфейс виклику iOS</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>Використовуйте новий профіль інкогніто</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Use server" xml:space="preserve"> + <source>Use server</source> + <target>Використовувати сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="User profile" xml:space="preserve"> + <source>User profile</source> + <target>Профіль користувача</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Using .onion hosts requires compatible VPN provider." xml:space="preserve"> + <source>Using .onion hosts requires compatible VPN provider.</source> + <target>Для використання хостів .onion потрібен сумісний VPN-провайдер.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Using SimpleX Chat servers." xml:space="preserve"> + <source>Using SimpleX Chat servers.</source> + <target>Використання серверів SimpleX Chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify connection security" xml:space="preserve"> + <source>Verify connection security</source> + <target>Перевірте безпеку з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Verify security code" xml:space="preserve"> + <source>Verify security code</source> + <target>Підтвердіть код безпеки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Via browser" xml:space="preserve"> + <source>Via browser</source> + <target>Через браузер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video call" xml:space="preserve"> + <source>Video call</source> + <target>Відеодзвінок</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video will be received when your contact completes uploading it." xml:space="preserve"> + <source>Video will be received when your contact completes uploading it.</source> + <target>Відео буде отримано, коли ваш контакт завершить завантаження.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Video will be received when your contact is online, please wait or check later!" xml:space="preserve"> + <source>Video will be received when your contact is online, please wait or check later!</source> + <target>Відео буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Videos and files up to 1gb" xml:space="preserve"> + <source>Videos and files up to 1gb</source> + <target>Відео та файли до 1 Гб</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="View security code" xml:space="preserve"> + <source>View security code</source> + <target>Переглянути код безпеки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages" xml:space="preserve"> + <source>Voice messages</source> + <target>Голосові повідомлення</target> + <note>chat feature</note> + </trans-unit> + <trans-unit id="Voice messages are prohibited in this chat." xml:space="preserve"> + <source>Voice messages are prohibited in this chat.</source> + <target>Голосові повідомлення в цьому чаті заборонені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages are prohibited in this group." xml:space="preserve"> + <source>Voice messages are prohibited in this group.</source> + <target>Голосові повідомлення в цій групі заборонені.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice messages prohibited!" xml:space="preserve"> + <source>Voice messages prohibited!</source> + <target>Голосові повідомлення заборонені!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Voice message…" xml:space="preserve"> + <source>Voice message…</source> + <target>Голосове повідомлення…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for file" xml:space="preserve"> + <source>Waiting for file</source> + <target>Очікування файлу</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for image" xml:space="preserve"> + <source>Waiting for image</source> + <target>Очікування зображення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Waiting for video" xml:space="preserve"> + <source>Waiting for video</source> + <target>Чекаємо на відео</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Warning: you may lose some data!" xml:space="preserve"> + <source>Warning: you may lose some data!</source> + <target>Попередження: ви можете втратити деякі дані!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="WebRTC ICE servers" xml:space="preserve"> + <source>WebRTC ICE servers</source> + <target>Сервери WebRTC ICE</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome %@!" xml:space="preserve"> + <source>Welcome %@!</source> + <target>Ласкаво просимо %@!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Welcome message" xml:space="preserve"> + <source>Welcome message</source> + <target>Вітальне повідомлення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="What's new" xml:space="preserve"> + <source>What's new</source> + <target>Що нового</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When available" xml:space="preserve"> + <source>When available</source> + <target>За наявності</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When people request to connect, you can accept or reject it." xml:space="preserve"> + <source>When people request to connect, you can accept or reject it.</source> + <target>Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." xml:space="preserve"> + <source>When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</source> + <target>Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="With optional welcome message." xml:space="preserve"> + <source>With optional welcome message.</source> + <target>З необов'язковим вітальним повідомленням.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong database passphrase" xml:space="preserve"> + <source>Wrong database passphrase</source> + <target>Неправильний пароль до бази даних</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Wrong passphrase!" xml:space="preserve"> + <source>Wrong passphrase!</source> + <target>Неправильний пароль!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="XFTP servers" xml:space="preserve"> + <source>XFTP servers</source> + <target>Сервери XFTP</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You" xml:space="preserve"> + <source>You</source> + <target>Ти</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You accepted connection" xml:space="preserve"> + <source>You accepted connection</source> + <target>Ви прийняли підключення</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You allow" xml:space="preserve"> + <source>You allow</source> + <target>Ви дозволяєте</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You already have a chat profile with the same display name. Please choose another name." xml:space="preserve"> + <source>You already have a chat profile with the same display name. Please choose another name.</source> + <target>Ви вже маєте профіль у чаті з таким самим іменем. Будь ласка, виберіть інше ім'я.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are already connected to %@." xml:space="preserve"> + <source>You are already connected to %@.</source> + <target>Ви вже підключені до %@.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are connected to the server used to receive messages from this contact." xml:space="preserve"> + <source>You are connected to the server used to receive messages from this contact.</source> + <target>Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You are invited to group" xml:space="preserve"> + <source>You are invited to group</source> + <target>Запрошуємо вас до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can accept calls from lock screen, without device and app authentication." xml:space="preserve"> + <source>You can accept calls from lock screen, without device and app authentication.</source> + <target>Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." xml:space="preserve"> + <source>You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button.</source> + <target>Ви також можете підключитися за посиланням. Якщо воно відкриється в браузері, натисніть кнопку **Відкрити в мобільному додатку**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can create it later" xml:space="preserve"> + <source>You can create it later</source> + <target>Ви можете створити його пізніше</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable later via Settings" xml:space="preserve"> + <source>You can enable later via Settings</source> + <target>Ви можете увімкнути пізніше в Налаштуваннях</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> + <source>You can enable them later via app Privacy & Security settings.</source> + <target>Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> + <source>You can hide or mute a user profile - swipe it to the right.</source> + <target>Ви можете приховати або вимкнути звук профілю користувача - проведіть по ньому вправо.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can now send messages to %@" xml:space="preserve"> + <source>You can now send messages to %@</source> + <target>Тепер ви можете надсилати повідомлення на адресу %@</target> + <note>notification body</note> + </trans-unit> + <trans-unit id="You can set lock screen notification preview via settings." xml:space="preserve"> + <source>You can set lock screen notification preview via settings.</source> + <target>Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." xml:space="preserve"> + <source>You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it.</source> + <target>Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо згодом видалите її.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can share this address with your contacts to let them connect with **%@**." xml:space="preserve"> + <source>You can share this address with your contacts to let them connect with **%@**.</source> + <target>Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can share your address as a link or QR code - anybody can connect to you." xml:space="preserve"> + <source>You can share your address as a link or QR code - anybody can connect to you.</source> + <target>Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can start chat via app Settings / Database or by restarting the app" xml:space="preserve"> + <source>You can start chat via app Settings / Database or by restarting the app</source> + <target>Запустити чат можна через Налаштування програми / База даних або перезапустивши програму</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can turn on SimpleX Lock via Settings." xml:space="preserve"> + <source>You can turn on SimpleX Lock via Settings.</source> + <target>Увімкнути SimpleX Lock можна в Налаштуваннях.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can use markdown to format messages:" xml:space="preserve"> + <source>You can use markdown to format messages:</source> + <target>Ви можете використовувати розмітку для форматування повідомлень:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You can't send messages!" xml:space="preserve"> + <source>You can't send messages!</source> + <target>Ви не можете надсилати повідомлення!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." xml:space="preserve"> + <source>You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them.</source> + <target>Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You could not be verified; please try again." xml:space="preserve"> + <source>You could not be verified; please try again.</source> + <target>Вас не вдалося верифікувати, спробуйте ще раз.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have no chats" xml:space="preserve"> + <source>You have no chats</source> + <target>У вас немає чатів</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You have to enter passphrase every time the app starts - it is not stored on the device." xml:space="preserve"> + <source>You have to enter passphrase every time the app starts - it is not stored on the device.</source> + <target>Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> + <target>Ви запросили контакт</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You joined this group" xml:space="preserve"> + <source>You joined this group</source> + <target>Ви приєдналися до цієї групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You joined this group. Connecting to inviting group member." xml:space="preserve"> + <source>You joined this group. Connecting to inviting group member.</source> + <target>Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." xml:space="preserve"> + <source>You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.</source> + <target>Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You need to allow your contact to send voice messages to be able to send them." xml:space="preserve"> + <source>You need to allow your contact to send voice messages to be able to send them.</source> + <target>Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You rejected group invitation" xml:space="preserve"> + <source>You rejected group invitation</source> + <target>Ви відхилили запрошення до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You sent group invitation" xml:space="preserve"> + <source>You sent group invitation</source> + <target>Ви надіслали запрошення до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected to group when the group host's device is online, please wait or check later!" xml:space="preserve"> + <source>You will be connected to group when the group host's device is online, please wait or check later!</source> + <target>Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected when your connection request is accepted, please wait or check later!" xml:space="preserve"> + <source>You will be connected when your connection request is accepted, please wait or check later!</source> + <target>Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be connected when your contact's device is online, please wait or check later!" xml:space="preserve"> + <source>You will be connected when your contact's device is online, please wait or check later!</source> + <target>Ви будете з'єднані, коли пристрій вашого контакту буде онлайн, будь ласка, зачекайте або перевірте пізніше!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will be required to authenticate when you start or resume the app after 30 seconds in background." xml:space="preserve"> + <source>You will be required to authenticate when you start or resume the app after 30 seconds in background.</source> + <target>Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will join a group this link refers to and connect to its group members." xml:space="preserve"> + <source>You will join a group this link refers to and connect to its group members.</source> + <target>Ви приєднаєтеся до групи, на яку посилається це посилання, і з'єднаєтеся з її учасниками.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will still receive calls and notifications from muted profiles when they are active." xml:space="preserve"> + <source>You will still receive calls and notifications from muted profiles when they are active.</source> + <target>Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You will stop receiving messages from this group. Chat history will be preserved." xml:space="preserve"> + <source>You will stop receiving messages from this group. Chat history will be preserved.</source> + <target>Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You won't lose your contacts if you later delete your address." xml:space="preserve"> + <source>You won't lose your contacts if you later delete your address.</source> + <target>Ви не втратите свої контакти, якщо згодом видалите свою адресу.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" xml:space="preserve"> + <source>You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile</source> + <target>Ви намагаєтеся запросити контакт, з яким ви поділилися профілем інкогніто, до групи, в якій ви використовуєте свій основний профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" xml:space="preserve"> + <source>You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed</source> + <target>Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your %@ servers" xml:space="preserve"> + <source>Your %@ servers</source> + <target>Ваші сервери %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your ICE servers" xml:space="preserve"> + <source>Your ICE servers</source> + <target>Ваші сервери ICE</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your SMP servers" xml:space="preserve"> + <source>Your SMP servers</source> + <target>Ваші SMP-сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your SimpleX address" xml:space="preserve"> + <source>Your SimpleX address</source> + <target>Ваша адреса SimpleX</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your XFTP servers" xml:space="preserve"> + <source>Your XFTP servers</source> + <target>Ваші XFTP-сервери</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your calls" xml:space="preserve"> + <source>Your calls</source> + <target>Твої дзвінки</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat database" xml:space="preserve"> + <source>Your chat database</source> + <target>Ваша база даних чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat database is not encrypted - set passphrase to encrypt it." xml:space="preserve"> + <source>Your chat database is not encrypted - set passphrase to encrypt it.</source> + <target>Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat profile will be sent to group members" xml:space="preserve"> + <source>Your chat profile will be sent to group members</source> + <target>Ваш профіль у чаті буде надіслано учасникам групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your chat profiles" xml:space="preserve"> + <source>Your chat profiles</source> + <target>Ваші профілі чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contact needs to be online for the connection to complete. You can cancel this connection and remove the contact (and try later with a new link)." xml:space="preserve"> + <source>Your contact needs to be online for the connection to complete. +You can cancel this connection and remove the contact (and try later with a new link).</source> + <target>Для завершення з'єднання ваш контакт має бути онлайн. +Ви можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contact sent a file that is larger than currently supported maximum size (%@)." xml:space="preserve"> + <source>Your contact sent a file that is larger than currently supported maximum size (%@).</source> + <target>Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@).</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts can allow full message deletion." xml:space="preserve"> + <source>Your contacts can allow full message deletion.</source> + <target>Ваші контакти можуть дозволити повне видалення повідомлень.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts in SimpleX will see it. You can change it in Settings." xml:space="preserve"> + <source>Your contacts in SimpleX will see it. +You can change it in Settings.</source> + <target>Ваші контакти в SimpleX побачать це. +Ви можете змінити його в Налаштуваннях.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your contacts will remain connected." xml:space="preserve"> + <source>Your contacts will remain connected.</source> + <target>Ваші контакти залишаться на зв'язку.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your current chat database will be DELETED and REPLACED with the imported one." xml:space="preserve"> + <source>Your current chat database will be DELETED and REPLACED with the imported one.</source> + <target>Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your current profile" xml:space="preserve"> + <source>Your current profile</source> + <target>Ваш поточний профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your preferences" xml:space="preserve"> + <source>Your preferences</source> + <target>Ваші уподобання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your privacy" xml:space="preserve"> + <source>Your privacy</source> + <target>Ваша конфіденційність</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>Ваш профіль **%@** буде опублікований.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> + <source>Your profile is stored on your device and shared only with your contacts. +SimpleX servers cannot see your profile.</source> + <target>Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам. +Сервери SimpleX не бачать ваш профіль.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> + <source>Your profile, contacts and delivered messages are stored on your device.</source> + <target>Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your random profile" xml:space="preserve"> + <source>Your random profile</source> + <target>Ваш випадковий профіль</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your server" xml:space="preserve"> + <source>Your server</source> + <target>Ваш сервер</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your server address" xml:space="preserve"> + <source>Your server address</source> + <target>Адреса вашого сервера</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Your settings" xml:space="preserve"> + <source>Your settings</source> + <target>Ваші налаштування</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" xml:space="preserve"> + <source>[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)</source> + <target>[Внесок](https://github.com/simplex-chat/simplex-chat#contribute)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="[Send us email](mailto:chat@simplex.chat)" xml:space="preserve"> + <source>[Send us email](mailto:chat@simplex.chat)</source> + <target>[Напишіть нам електронною поштою](mailto:chat@simplex.chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" xml:space="preserve"> + <source>[Star on GitHub](https://github.com/simplex-chat/simplex-chat)</source> + <target>[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="_italic_" xml:space="preserve"> + <source>\_italic_</source> + <target>\_курсив_</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="`a + b`" xml:space="preserve"> + <source>\`a + b`</source> + <target>\`a + b`</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="above, then choose:" xml:space="preserve"> + <source>above, then choose:</source> + <target>вище, а потім обирайте:</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="accepted call" xml:space="preserve"> + <source>accepted call</source> + <target>прийнято виклик</target> + <note>call status</note> + </trans-unit> + <trans-unit id="admin" xml:space="preserve"> + <source>admin</source> + <target>адмін</target> + <note>member role</note> + </trans-unit> + <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> + <source>agreeing encryption for %@…</source> + <target>узгодження шифрування для %@…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="agreeing encryption…" xml:space="preserve"> + <source>agreeing encryption…</source> + <target>узгодження шифрування…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="always" xml:space="preserve"> + <source>always</source> + <target>завжди</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="audio call (not e2e encrypted)" xml:space="preserve"> + <source>audio call (not e2e encrypted)</source> + <target>аудіовиклик (без шифрування e2e)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="bad message ID" xml:space="preserve"> + <source>bad message ID</source> + <target>невірний ідентифікатор повідомлення</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="bad message hash" xml:space="preserve"> + <source>bad message hash</source> + <target>невірний хеш повідомлення</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="bold" xml:space="preserve"> + <source>bold</source> + <target>жирний</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="call error" xml:space="preserve"> + <source>call error</source> + <target>помилка дзвінка</target> + <note>call status</note> + </trans-unit> + <trans-unit id="call in progress" xml:space="preserve"> + <source>call in progress</source> + <target>виклик у процесі</target> + <note>call status</note> + </trans-unit> + <trans-unit id="calling…" xml:space="preserve"> + <source>calling…</source> + <target>дзвоніть…</target> + <note>call status</note> + </trans-unit> + <trans-unit id="cancelled %@" xml:space="preserve"> + <source>cancelled %@</source> + <target>скасовано %@</target> + <note>feature offered item</note> + </trans-unit> + <trans-unit id="changed address for you" xml:space="preserve"> + <source>changed address for you</source> + <target>змінили для вас адресу</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="changed role of %@ to %@" xml:space="preserve"> + <source>changed role of %1$@ to %2$@</source> + <target>змінено роль %1$@ на %2$@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="changed your role to %@" xml:space="preserve"> + <source>changed your role to %@</source> + <target>змінили свою роль на %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="changing address for %@…" xml:space="preserve"> + <source>changing address for %@…</source> + <target>зміна адреси для %@…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="changing address…" xml:space="preserve"> + <source>changing address…</source> + <target>змінює адресу…</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="colored" xml:space="preserve"> + <source>colored</source> + <target>кольоровий</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="complete" xml:space="preserve"> + <source>complete</source> + <target>завершено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connect to SimpleX Chat developers." xml:space="preserve"> + <source>connect to SimpleX Chat developers.</source> + <target>зв'язатися з розробниками SimpleX Chat.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connected" xml:space="preserve"> + <source>connected</source> + <target>з'єднаний</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="connecting" xml:space="preserve"> + <source>connecting</source> + <target>з'єднання</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (accepted)" xml:space="preserve"> + <source>connecting (accepted)</source> + <target>з'єднання (прийнято)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (announced)" xml:space="preserve"> + <source>connecting (announced)</source> + <target>з'єднання (оголошено)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (introduced)" xml:space="preserve"> + <source>connecting (introduced)</source> + <target>з'єднання (введено)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting (introduction invitation)" xml:space="preserve"> + <source>connecting (introduction invitation)</source> + <target>з'єднання (вступне запрошення)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="connecting call" xml:space="preserve"> + <source>connecting call…</source> + <target>підключення дзвінка…</target> + <note>call status</note> + </trans-unit> + <trans-unit id="connecting…" xml:space="preserve"> + <source>connecting…</source> + <target>з'єднання…</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="connection established" xml:space="preserve"> + <source>connection established</source> + <target>з'єднання встановлене</target> + <note>chat list item title (it should not be shown</note> + </trans-unit> + <trans-unit id="connection:%@" xml:space="preserve"> + <source>connection:%@</source> + <target>з'єднання:%@</target> + <note>connection information</note> + </trans-unit> + <trans-unit id="contact has e2e encryption" xml:space="preserve"> + <source>contact has e2e encryption</source> + <target>контакт має шифрування e2e</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="contact has no e2e encryption" xml:space="preserve"> + <source>contact has no e2e encryption</source> + <target>контакт не має шифрування e2e</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="creator" xml:space="preserve"> + <source>creator</source> + <target>творець</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="custom" xml:space="preserve"> + <source>custom</source> + <target>звичайний</target> + <note>dropdown time picker choice</note> + </trans-unit> + <trans-unit id="database version is newer than the app, but no down migration for: %@" xml:space="preserve"> + <source>database version is newer than the app, but no down migration for: %@</source> + <target>версія бази даних новіша, ніж додаток, але без міграції вниз для: %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="days" xml:space="preserve"> + <source>days</source> + <target>днів</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="default (%@)" xml:space="preserve"> + <source>default (%@)</source> + <target>за замовчуванням (%@)</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="default (no)" xml:space="preserve"> + <source>default (no)</source> + <target>за замовчуванням (ні)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="default (yes)" xml:space="preserve"> + <source>default (yes)</source> + <target>за замовчуванням (так)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="deleted" xml:space="preserve"> + <source>deleted</source> + <target>видалено</target> + <note>deleted chat item</note> + </trans-unit> + <trans-unit id="deleted group" xml:space="preserve"> + <source>deleted group</source> + <target>видалено групу</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="different migration in the app/database: %@ / %@" xml:space="preserve"> + <source>different migration in the app/database: %@ / %@</source> + <target>різна міграція в додатку/базі даних: %@ / %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="direct" xml:space="preserve"> + <source>direct</source> + <target>прямо</target> + <note>connection level description</note> + </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>вимкнено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="duplicate message" xml:space="preserve"> + <source>duplicate message</source> + <target>дублююче повідомлення</target> + <note>integrity error chat item</note> + </trans-unit> + <trans-unit id="e2e encrypted" xml:space="preserve"> + <source>e2e encrypted</source> + <target>e2e зашифрований</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="enabled" xml:space="preserve"> + <source>enabled</source> + <target>увімкнено</target> + <note>enabled status</note> + </trans-unit> + <trans-unit id="enabled for contact" xml:space="preserve"> + <source>enabled for contact</source> + <target>увімкнено для контакту</target> + <note>enabled status</note> + </trans-unit> + <trans-unit id="enabled for you" xml:space="preserve"> + <source>enabled for you</source> + <target>увімкнено для вас</target> + <note>enabled status</note> + </trans-unit> + <trans-unit id="encryption agreed" xml:space="preserve"> + <source>encryption agreed</source> + <target>узгоджено шифрування</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption agreed for %@" xml:space="preserve"> + <source>encryption agreed for %@</source> + <target>узгоджене шифрування для %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok" xml:space="preserve"> + <source>encryption ok</source> + <target>шифрування ok</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption ok for %@" xml:space="preserve"> + <source>encryption ok for %@</source> + <target>шифрування ok для %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> + <source>encryption re-negotiation allowed</source> + <target>переузгодження шифрування дозволено</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> + <source>encryption re-negotiation allowed for %@</source> + <target>переузгодження шифрування дозволено для %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required" xml:space="preserve"> + <source>encryption re-negotiation required</source> + <target>потрібне повторне узгодження шифрування</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> + <source>encryption re-negotiation required for %@</source> + <target>для %@ потрібне повторне узгодження шифрування</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="ended" xml:space="preserve"> + <source>ended</source> + <target>закінчився</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="ended call %@" xml:space="preserve"> + <source>ended call %@</source> + <target>закінчився виклик %@</target> + <note>call status</note> + </trans-unit> + <trans-unit id="error" xml:space="preserve"> + <source>error</source> + <target>помилка</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>відбулася подія</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="group deleted" xml:space="preserve"> + <source>group deleted</source> + <target>групу видалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="group profile updated" xml:space="preserve"> + <source>group profile updated</source> + <target>оновлено профіль групи</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="hours" xml:space="preserve"> + <source>hours</source> + <target>години</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="iOS Keychain is used to securely store passphrase - it allows receiving push notifications." xml:space="preserve"> + <source>iOS Keychain is used to securely store passphrase - it allows receiving push notifications.</source> + <target>iOS Keychain використовується для безпечного зберігання пароля - це дає змогу отримувати миттєві повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." xml:space="preserve"> + <source>iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications.</source> + <target>Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення.</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="incognito via contact address link" xml:space="preserve"> + <source>incognito via contact address link</source> + <target>інкогніто за посиланням на контактну адресу</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="incognito via group link" xml:space="preserve"> + <source>incognito via group link</source> + <target>інкогніто через групове посилання</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="incognito via one-time link" xml:space="preserve"> + <source>incognito via one-time link</source> + <target>інкогніто за одноразовим посиланням</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="indirect (%d)" xml:space="preserve"> + <source>indirect (%d)</source> + <target>непрямий (%d)</target> + <note>connection level description</note> + </trans-unit> + <trans-unit id="invalid chat" xml:space="preserve"> + <source>invalid chat</source> + <target>недійсний чат</target> + <note>invalid chat data</note> + </trans-unit> + <trans-unit id="invalid chat data" xml:space="preserve"> + <source>invalid chat data</source> + <target>невірні дані чату</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="invalid data" xml:space="preserve"> + <source>invalid data</source> + <target>невірні дані</target> + <note>invalid chat item</note> + </trans-unit> + <trans-unit id="invitation to group %@" xml:space="preserve"> + <source>invitation to group %@</source> + <target>запрошення до групи %@</target> + <note>group name</note> + </trans-unit> + <trans-unit id="invited" xml:space="preserve"> + <source>invited</source> + <target>запрошені</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="invited %@" xml:space="preserve"> + <source>invited %@</source> + <target>запрошений %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="invited to connect" xml:space="preserve"> + <source>invited to connect</source> + <target>запрошуємо приєднатися</target> + <note>chat list item title</note> + </trans-unit> + <trans-unit id="invited via your group link" xml:space="preserve"> + <source>invited via your group link</source> + <target>запрошені за посиланням у вашій групі</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="italic" xml:space="preserve"> + <source>italic</source> + <target>курсив</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="join as %@" xml:space="preserve"> + <source>join as %@</source> + <target>приєднатися як %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="left" xml:space="preserve"> + <source>left</source> + <target>ліворуч</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="marked deleted" xml:space="preserve"> + <source>marked deleted</source> + <target>з позначкою видалено</target> + <note>marked deleted chat item preview text</note> + </trans-unit> + <trans-unit id="member" xml:space="preserve"> + <source>member</source> + <target>учасник</target> + <note>member role</note> + </trans-unit> + <trans-unit id="member connected" xml:space="preserve"> + <source>connected</source> + <target>з'єднаний</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="message received" xml:space="preserve"> + <source>message received</source> + <target>повідомлення отримано</target> + <note>notification</note> + </trans-unit> + <trans-unit id="minutes" xml:space="preserve"> + <source>minutes</source> + <target>хвилини</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="missed call" xml:space="preserve"> + <source>missed call</source> + <target>пропущений дзвінок</target> + <note>call status</note> + </trans-unit> + <trans-unit id="moderated" xml:space="preserve"> + <source>moderated</source> + <target>модерується</target> + <note>moderated chat item</note> + </trans-unit> + <trans-unit id="moderated by %@" xml:space="preserve"> + <source>moderated by %@</source> + <target>модерується %@</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="months" xml:space="preserve"> + <source>months</source> + <target>місяців</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="never" xml:space="preserve"> + <source>never</source> + <target>ніколи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="new message" xml:space="preserve"> + <source>new message</source> + <target>нове повідомлення</target> + <note>notification</note> + </trans-unit> + <trans-unit id="no" xml:space="preserve"> + <source>no</source> + <target>ні</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="no e2e encryption" xml:space="preserve"> + <source>no e2e encryption</source> + <target>без шифрування e2e</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="no text" xml:space="preserve"> + <source>no text</source> + <target>без тексту</target> + <note>copied message info in history</note> + </trans-unit> + <trans-unit id="observer" xml:space="preserve"> + <source>observer</source> + <target>спостерігач</target> + <note>member role</note> + </trans-unit> + <trans-unit id="off" xml:space="preserve"> + <source>off</source> + <target>вимкнено</target> + <note>enabled status + group pref value</note> + </trans-unit> + <trans-unit id="offered %@" xml:space="preserve"> + <source>offered %@</source> + <target>запропоновано %@</target> + <note>feature offered item</note> + </trans-unit> + <trans-unit id="offered %@: %@" xml:space="preserve"> + <source>offered %1$@: %2$@</source> + <target>запропонував %1$@: %2$@</target> + <note>feature offered item</note> + </trans-unit> + <trans-unit id="on" xml:space="preserve"> + <source>on</source> + <target>увімкнено</target> + <note>group pref value</note> + </trans-unit> + <trans-unit id="or chat with the developers" xml:space="preserve"> + <source>or chat with the developers</source> + <target>або поспілкуйтеся з розробниками</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="owner" xml:space="preserve"> + <source>owner</source> + <target>власник</target> + <note>member role</note> + </trans-unit> + <trans-unit id="peer-to-peer" xml:space="preserve"> + <source>peer-to-peer</source> + <target>одноранговий</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="received answer…" xml:space="preserve"> + <source>received answer…</source> + <target>отримали відповідь…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="received confirmation…" xml:space="preserve"> + <source>received confirmation…</source> + <target>отримали підтвердження…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="rejected call" xml:space="preserve"> + <source>rejected call</source> + <target>відхилений виклик</target> + <note>call status</note> + </trans-unit> + <trans-unit id="removed" xml:space="preserve"> + <source>removed</source> + <target>видалено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="removed %@" xml:space="preserve"> + <source>removed %@</source> + <target>видалено %@</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="removed you" xml:space="preserve"> + <source>removed you</source> + <target>прибрали вас</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="sec" xml:space="preserve"> + <source>sec</source> + <target>сек</target> + <note>network option</note> + </trans-unit> + <trans-unit id="seconds" xml:space="preserve"> + <source>seconds</source> + <target>секунди</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="secret" xml:space="preserve"> + <source>secret</source> + <target>таємниця</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="security code changed" xml:space="preserve"> + <source>security code changed</source> + <target>змінено код безпеки</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="starting…" xml:space="preserve"> + <source>starting…</source> + <target>починаючи…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="strike" xml:space="preserve"> + <source>strike</source> + <target>закреслено</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="this contact" xml:space="preserve"> + <source>this contact</source> + <target>цей контакт</target> + <note>notification title</note> + </trans-unit> + <trans-unit id="unknown" xml:space="preserve"> + <source>unknown</source> + <target>невідомий</target> + <note>connection info</note> + </trans-unit> + <trans-unit id="updated group profile" xml:space="preserve"> + <source>updated group profile</source> + <target>оновлений профіль групи</target> + <note>rcv group event chat item</note> + </trans-unit> + <trans-unit id="v%@ (%@)" xml:space="preserve"> + <source>v%@ (%@)</source> + <target>v%@ (%@)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="via contact address link" xml:space="preserve"> + <source>via contact address link</source> + <target>за посиланням на контактну адресу</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="via group link" xml:space="preserve"> + <source>via group link</source> + <target>за посиланням на групу</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="via one-time link" xml:space="preserve"> + <source>via one-time link</source> + <target>за одноразовим посиланням</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="via relay" xml:space="preserve"> + <source>via relay</source> + <target>за допомогою ретранслятора</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="video call (not e2e encrypted)" xml:space="preserve"> + <source>video call (not e2e encrypted)</source> + <target>відеодзвінок (без шифрування e2e)</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="waiting for answer…" xml:space="preserve"> + <source>waiting for answer…</source> + <target>в очікуванні відповіді…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="waiting for confirmation…" xml:space="preserve"> + <source>waiting for confirmation…</source> + <target>чекаємо на підтвердження…</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="wants to connect to you!" xml:space="preserve"> + <source>wants to connect to you!</source> + <target>хоче зв'язатися з вами!</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="weeks" xml:space="preserve"> + <source>weeks</source> + <target>тижнів</target> + <note>time unit</note> + </trans-unit> + <trans-unit id="yes" xml:space="preserve"> + <source>yes</source> + <target>так</target> + <note>pref value</note> + </trans-unit> + <trans-unit id="you are invited to group" xml:space="preserve"> + <source>you are invited to group</source> + <target>вас запрошують до групи</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="you are observer" xml:space="preserve"> + <source>you are observer</source> + <target>ви спостерігач</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="you changed address" xml:space="preserve"> + <source>you changed address</source> + <target>ви змінили адресу</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="you changed address for %@" xml:space="preserve"> + <source>you changed address for %@</source> + <target>ви змінили адресу на %@</target> + <note>chat item text</note> + </trans-unit> + <trans-unit id="you changed role for yourself to %@" xml:space="preserve"> + <source>you changed role for yourself to %@</source> + <target>ви змінили роль для себе на %@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you changed role of %@ to %@" xml:space="preserve"> + <source>you changed role of %1$@ to %2$@</source> + <target>ви змінили роль %1$@ на %2$@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you left" xml:space="preserve"> + <source>you left</source> + <target>ти пішов</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you removed %@" xml:space="preserve"> + <source>you removed %@</source> + <target>ви видалили %@</target> + <note>snd group event chat item</note> + </trans-unit> + <trans-unit id="you shared one-time link" xml:space="preserve"> + <source>you shared one-time link</source> + <target>ви поділилися одноразовим посиланням</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="you shared one-time link incognito" xml:space="preserve"> + <source>you shared one-time link incognito</source> + <target>ви поділилися одноразовим посиланням інкогніто</target> + <note>chat list item description</note> + </trans-unit> + <trans-unit id="you: " xml:space="preserve"> + <source>you: </source> + <target>ти: </target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="~strike~" xml:space="preserve"> + <source>\~strike~</source> + <target>\~закреслити~</target> <note>No comment provided by engineer.</note> </trans-unit> </body> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="uk" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX</source> + <target>SimpleX</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSCameraUsageDescription" xml:space="preserve"> <source>SimpleX needs camera access to scan QR codes to connect to other users and for video calls.</source> + <target>SimpleX потребує доступу до камери, щоб сканувати QR-коди для з'єднання з іншими користувачами та для відеодзвінків.</target> <note>Privacy - Camera Usage Description</note> </trans-unit> <trans-unit id="NSFaceIDUsageDescription" xml:space="preserve"> <source>SimpleX uses Face ID for local authentication</source> + <target>SimpleX використовує Face ID для локальної автентифікації</target> <note>Privacy - Face ID Usage Description</note> </trans-unit> <trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve"> <source>SimpleX needs microphone access for audio and video calls, and to record voice messages.</source> + <target>SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень.</target> <note>Privacy - Microphone Usage Description</note> </trans-unit> <trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve"> <source>SimpleX needs access to Photo Library for saving captured and received media</source> + <target>SimpleX потребує доступу до фототеки для збереження захоплених та отриманих медіафайлів</target> <note>Privacy - Photo Library Additions Usage Description</note> </trans-unit> </body> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="uk" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.2" build-num="14C18"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle display name</note> </trans-unit> <trans-unit id="CFBundleName" xml:space="preserve"> <source>SimpleX NSE</source> + <target>SimpleX NSE</target> <note>Bundle name</note> </trans-unit> <trans-unit id="NSHumanReadableCopyright" xml:space="preserve"> <source>Copyright © 2022 SimpleX Chat. All rights reserved.</source> + <target>Авторське право © 2022 SimpleX Chat. Всі права захищені.</target> <note>Copyright (human-readable)</note> </trans-unit> </body> diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..aaa7f79bc8 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,23 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "red" : "0.000", + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.533" + } + }, + "idiom" : "universal" + } + ], + "properties" : { + "localizable" : true + }, + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..124ddbcc33 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,6 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings new file mode 100644 index 0000000000..cf485752ea --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/Localizable.strings @@ -0,0 +1,30 @@ +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Add new contact**: to create your one-time QR Code or link for your contact."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* call status */ +"connecting call" = "connecting call…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Connecting to server…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Connecting to server… (error: %@)"; + +/* rcv group event chat item */ +"member connected" = "connected"; + +/* No comment provided by engineer. */ +"No group!" = "Group not found!"; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..3af673b19f --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/en.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,10 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX needs camera access to scan QR codes to connect to other users and for video calls."; +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX uses Face ID for local authentication"; +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX needs microphone access for audio and video calls, and to record voice messages."; +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX needs access to Photo Library for saving captured and received media"; diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json new file mode 100644 index 0000000000..6c122f11ab --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json @@ -0,0 +1,12 @@ +{ + "developmentRegion" : "en", + "project" : "SimpleX.xcodeproj", + "targetLocale" : "uk", + "toolInfo" : { + "toolBuildNumber" : "15A240d", + "toolID" : "com.apple.dt.xcode", + "toolName" : "Xcode", + "toolVersion" : "15.0" + }, + "version" : "1.0" +} \ No newline at end of file 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 6c69385fab..9fcd6d0bf2 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 @@ -2,7 +2,7 @@ <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd"> <file original="en.lproj/Localizable.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id=" " xml:space="preserve"> @@ -42,6 +42,21 @@ <target>!1 种彩色!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="# %@" xml:space="preserve"> + <source># %@</source> + <target># %@</target> + <note>copied message info title, # <title></note> + </trans-unit> + <trans-unit id="## History" xml:space="preserve"> + <source>## History</source> + <target>## 历史</target> + <note>copied message info</note> + </trans-unit> + <trans-unit id="## In reply to" xml:space="preserve"> + <source>## In reply to</source> + <target>## 回复</target> + <note>copied message info</note> + </trans-unit> <trans-unit id="#secret#" xml:space="preserve"> <source>#secret#</source> <target>#秘密#</target> @@ -72,8 +87,14 @@ <target>%@ / %@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%@ and %@ connected" xml:space="preserve"> + <source>%@ and %@ connected</source> + <target>%@ 和%@ 以建立连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@ at %@:" xml:space="preserve"> <source>%1$@ at %2$@:</source> + <target>@ %2$@:</target> <note>copied message info, <sender> at <time></note> </trans-unit> <trans-unit id="%@ is connected!" xml:space="preserve"> @@ -101,6 +122,11 @@ <target>%@ 要连接!</target> <note>notification title</note> </trans-unit> + <trans-unit id="%@, %@ and %lld other members connected" xml:space="preserve"> + <source>%@, %@ and %lld other members connected</source> + <target>%@, %@ 和 %lld 个成员</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%@:" xml:space="preserve"> <source>%@:</source> <target>%@:</target> @@ -171,6 +197,11 @@ <target>%lld 分钟</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="%lld new interface languages" xml:space="preserve"> + <source>%lld new interface languages</source> + <target>%lld 种新的界面语言</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="%lld second(s)" xml:space="preserve"> <source>%lld second(s)</source> <target>%lld 秒</target> @@ -301,10 +332,19 @@ <target>, </target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! - delivery receipts (up to 20 members). - faster and more stable." xml:space="preserve"> + <source>- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="- more stable message delivery. - a bit better groups. - and more!" xml:space="preserve"> <source>- more stable message delivery. - a bit better groups. - and more!</source> + <target>- 更稳定的传输! +- 更好的社群! +- 以及更多!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="- voice messages up to 5 minutes. - custom time to disappear. - editing history." xml:space="preserve"> @@ -385,6 +425,7 @@ </trans-unit> <trans-unit id="A few more things" xml:space="preserve"> <source>A few more things</source> + <target>一些杂项</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A new contact" xml:space="preserve"> @@ -392,14 +433,9 @@ <target>新联系人</target> <note>notification title</note> </trans-unit> - <trans-unit id="A random profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>A random profile will be sent to the contact that you received this link from</source> - <target>一个随机个人资料将被发送至给予您链接的联系人那里</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="A random profile will be sent to your contact" xml:space="preserve"> - <source>A random profile will be sent to your contact</source> - <target>一个随机资料将发送给您的联系人</target> + <trans-unit id="A new random profile will be shared." xml:space="preserve"> + <source>A new random profile will be shared.</source> + <target>创建一个随机的共享文件。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="A separate TCP connection will be used **for each chat profile you have in the app**." xml:space="preserve"> @@ -455,9 +491,9 @@ <note>accept contact request via notification accept incoming call via notification</note> </trans-unit> - <trans-unit id="Accept contact" xml:space="preserve"> - <source>Accept contact</source> - <target>接受联系人</target> + <trans-unit id="Accept connection request?" xml:space="preserve"> + <source>Accept connection request?</source> + <target>接受联系人?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Accept contact request from %@?" xml:space="preserve"> @@ -468,7 +504,7 @@ <trans-unit id="Accept incognito" xml:space="preserve"> <source>Accept incognito</source> <target>接受隐身聊天</target> - <note>No comment provided by engineer.</note> + <note>accept contact request via notification</note> </trans-unit> <trans-unit id="Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." xml:space="preserve"> <source>Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts.</source> @@ -675,6 +711,10 @@ <target>应用程序构建:%@</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="App encrypts new local files (except videos)." xml:space="preserve"> + <source>App encrypts new local files (except videos).</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="App icon" xml:space="preserve"> <source>App icon</source> <target>应用程序图标</target> @@ -810,6 +850,10 @@ <target>您和您的联系人都可以发送语音消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" xml:space="preserve"> + <source>Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." xml:space="preserve"> <source>By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA).</source> <target>通过聊天资料(默认)或者[通过连接](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)。</target> @@ -1041,9 +1085,19 @@ <target>连接</target> <note>server test step</note> </trans-unit> - <trans-unit id="Connect via contact link?" xml:space="preserve"> - <source>Connect via contact link?</source> - <target>通过联系人链接进行连接?</target> + <trans-unit id="Connect directly" xml:space="preserve"> + <source>Connect directly</source> + <target>直接连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect incognito" xml:space="preserve"> + <source>Connect incognito</source> + <target>在隐身状态下连接</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Connect via contact link" xml:space="preserve"> + <source>Connect via contact link</source> + <target>通过联系人链接进行连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connect via group link?" xml:space="preserve"> @@ -1061,9 +1115,9 @@ <target>通过群组链接/二维码连接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connect via one-time link?" xml:space="preserve"> - <source>Connect via one-time link?</source> - <target>通过一次性链接连接?</target> + <trans-unit id="Connect via one-time link" xml:space="preserve"> + <source>Connect via one-time link</source> + <target>通过一次性链接连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Connecting server…" xml:space="preserve"> @@ -1091,11 +1145,6 @@ <target>连接错误(AUTH)</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Connection request" xml:space="preserve"> - <source>Connection request</source> - <target>连接请求</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Connection request sent!" xml:space="preserve"> <source>Connection request sent!</source> <target>已发送连接请求!</target> @@ -1148,6 +1197,7 @@ </trans-unit> <trans-unit id="Contacts" xml:space="preserve"> <source>Contacts</source> + <target>联系人</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Contacts can mark messages for deletion; you will be able to view them." xml:space="preserve"> @@ -1200,6 +1250,10 @@ <target>创建链接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" xml:space="preserve"> + <source>Create new profile in [desktop app](https://simplex.chat/downloads/). 💻</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Create one-time invitation link" xml:space="preserve"> <source>Create one-time invitation link</source> <target>创建一次性邀请链接</target> @@ -1543,12 +1597,19 @@ <target>已删除于:%@</target> <note>copied message info</note> </trans-unit> + <trans-unit id="Delivery" xml:space="preserve"> + <source>Delivery</source> + <target>传送</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Delivery receipts are disabled!" xml:space="preserve"> <source>Delivery receipts are disabled!</source> + <target>送达回执已禁用!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Delivery receipts!" xml:space="preserve"> <source>Delivery receipts!</source> + <target>送达回执!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Description" xml:space="preserve"> @@ -1598,6 +1659,7 @@ </trans-unit> <trans-unit id="Disable (keep overrides)" xml:space="preserve"> <source>Disable (keep overrides)</source> + <target>禁用(保留覆盖)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disable SimpleX Lock" xml:space="preserve"> @@ -1607,6 +1669,7 @@ </trans-unit> <trans-unit id="Disable for all" xml:space="preserve"> <source>Disable for all</source> + <target>全部禁用</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Disappearing message" xml:space="preserve"> @@ -1644,6 +1707,10 @@ <target>断开连接</target> <note>server test step</note> </trans-unit> + <trans-unit id="Discover and join groups" xml:space="preserve"> + <source>Discover and join groups</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Display name" xml:space="preserve"> <source>Display name</source> <target>显示名称</target> @@ -1671,6 +1738,7 @@ </trans-unit> <trans-unit id="Don't enable" xml:space="preserve"> <source>Don't enable</source> + <target>不要启用</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Don't show again" xml:space="preserve"> @@ -1715,6 +1783,7 @@ </trans-unit> <trans-unit id="Enable (keep overrides)" xml:space="preserve"> <source>Enable (keep overrides)</source> + <target>启用(保持覆盖)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable SimpleX Lock" xml:space="preserve"> @@ -1734,6 +1803,7 @@ </trans-unit> <trans-unit id="Enable for all" xml:space="preserve"> <source>Enable for all</source> + <target>全部启用</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Enable instant notifications?" xml:space="preserve"> @@ -1776,6 +1846,15 @@ <target>加密数据库?</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Encrypt local files" xml:space="preserve"> + <source>Encrypt local files</source> + <target>加密本地文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Encrypt stored files & media" xml:space="preserve"> + <source>Encrypt stored files & media</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Encrypted database" xml:space="preserve"> <source>Encrypted database</source> <target>加密数据库</target> @@ -1901,11 +1980,20 @@ <target>创建群组链接错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error creating member contact" xml:space="preserve"> + <source>Error creating member contact</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error creating profile!" xml:space="preserve"> <source>Error creating profile!</source> <target>创建资料错误!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error decrypting file" xml:space="preserve"> + <source>Error decrypting file</source> + <target>解密文件时出错</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error deleting chat database" xml:space="preserve"> <source>Error deleting chat database</source> <target>删除聊天数据库错误</target> @@ -1948,6 +2036,7 @@ </trans-unit> <trans-unit id="Error enabling delivery receipts!" xml:space="preserve"> <source>Error enabling delivery receipts!</source> + <target>启用送达回执出错!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error enabling notifications" xml:space="preserve"> @@ -2025,6 +2114,10 @@ <target>发送电邮错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Error sending member contact invitation" xml:space="preserve"> + <source>Error sending member contact invitation</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Error sending message" xml:space="preserve"> <source>Error sending message</source> <target>发送消息错误</target> @@ -2032,6 +2125,7 @@ </trans-unit> <trans-unit id="Error setting delivery receipts!" xml:space="preserve"> <source>Error setting delivery receipts!</source> + <target>设置送达回执出错!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error starting chat" xml:space="preserve"> @@ -2051,6 +2145,7 @@ </trans-unit> <trans-unit id="Error synchronizing connection" xml:space="preserve"> <source>Error synchronizing connection</source> + <target>同步连接错误</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Error updating group link" xml:space="preserve"> @@ -2095,6 +2190,7 @@ </trans-unit> <trans-unit id="Even when disabled in the conversation." xml:space="preserve"> <source>Even when disabled in the conversation.</source> + <target>即使在对话中被禁用。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Exit without saving" xml:space="preserve"> @@ -2179,6 +2275,7 @@ </trans-unit> <trans-unit id="Filter unread and favorite chats." xml:space="preserve"> <source>Filter unread and favorite chats.</source> + <target>过滤未读和收藏的聊天记录。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Finally, we have them! 🚀" xml:space="preserve"> @@ -2188,30 +2285,37 @@ </trans-unit> <trans-unit id="Find chats faster" xml:space="preserve"> <source>Find chats faster</source> + <target>更快地查找聊天记录</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix" xml:space="preserve"> <source>Fix</source> + <target>修复</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection" xml:space="preserve"> <source>Fix connection</source> + <target>修复连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix connection?" xml:space="preserve"> <source>Fix connection?</source> + <target>修复连接?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix encryption after restoring backups." xml:space="preserve"> <source>Fix encryption after restoring backups.</source> + <target>修复还原备份后的加密问题。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by contact" xml:space="preserve"> <source>Fix not supported by contact</source> + <target>修复联系人不支持的问题</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Fix not supported by group member" xml:space="preserve"> <source>Fix not supported by group member</source> + <target>修复群组成员不支持的问题</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="For console" xml:space="preserve"> @@ -2412,7 +2516,7 @@ <trans-unit id="History" xml:space="preserve"> <source>History</source> <target>历史记录</target> - <note>copied message info</note> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="How SimpleX works" xml:space="preserve"> <source>How SimpleX works</source> @@ -2521,7 +2625,8 @@ </trans-unit> <trans-unit id="In reply to" xml:space="preserve"> <source>In reply to</source> - <note>copied message info</note> + <target>答复</target> + <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incognito" xml:space="preserve"> <source>Incognito</source> @@ -2533,14 +2638,9 @@ <target>隐身模式</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Incognito mode is not supported here - your main profile will be sent to group members" xml:space="preserve"> - <source>Incognito mode is not supported here - your main profile will be sent to group members</source> - <target>此处不支持隐身模式——您的主要个人资料将发送给群组成员</target> - <note>No comment provided by engineer.</note> - </trans-unit> - <trans-unit id="Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." xml:space="preserve"> - <source>Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</source> - <target>隐身模式可以保护你的主要个人资料名称和图像的隐私——对于每个新的联系人,都会创建一个新的随机个人资料。</target> + <trans-unit id="Incognito mode protects your privacy by using a new random profile for each contact." xml:space="preserve"> + <source>Incognito mode protects your privacy by using a new random profile for each contact.</source> + <target>隐身模式会为每个联系人使用一个新的随机配置文件,从而保护你的隐私。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Incoming audio call" xml:space="preserve"> @@ -2615,6 +2715,11 @@ <target>无效的服务器地址!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Invalid status" xml:space="preserve"> + <source>Invalid status</source> + <target>无效状态</target> + <note>item status text</note> + </trans-unit> <trans-unit id="Invitation expired!" xml:space="preserve"> <source>Invitation expired!</source> <target>邀请已过期!</target> @@ -2708,6 +2813,7 @@ </trans-unit> <trans-unit id="Keep your connections" xml:space="preserve"> <source>Keep your connections</source> + <target>保持连接</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="KeyChain error" xml:space="preserve"> @@ -2802,6 +2908,7 @@ </trans-unit> <trans-unit id="Make one message disappear" xml:space="preserve"> <source>Make one message disappear</source> + <target>使一条消息消失</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Make profile private!" xml:space="preserve"> @@ -2872,10 +2979,11 @@ <trans-unit id="Message delivery error" xml:space="preserve"> <source>Message delivery error</source> <target>消息传递错误</target> - <note>No comment provided by engineer.</note> + <note>item status text</note> </trans-unit> <trans-unit id="Message delivery receipts!" xml:space="preserve"> <source>Message delivery receipts!</source> + <target>消息送达回执!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Message draft" xml:space="preserve"> @@ -2958,6 +3066,11 @@ <target>更多改进即将推出!</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Most likely this connection is deleted." xml:space="preserve"> + <source>Most likely this connection is deleted.</source> + <target>此连接很可能已被删除。</target> + <note>item status description</note> + </trans-unit> <trans-unit id="Most likely this contact has deleted the connection with you." xml:space="preserve"> <source>Most likely this contact has deleted the connection with you.</source> <target>很可能此联系人已经删除了与您的联系。</target> @@ -3018,6 +3131,10 @@ <target>新数据库存档</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="New desktop app!" xml:space="preserve"> + <source>New desktop app!</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="New display name" xml:space="preserve"> <source>New display name</source> <target>新显示名</target> @@ -3063,6 +3180,11 @@ <target>没有联系人可添加</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="No delivery information" xml:space="preserve"> + <source>No delivery information</source> + <target>无送达信息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="No device token!" xml:space="preserve"> <source>No device token!</source> <target>无设备令牌!</target> @@ -3080,6 +3202,7 @@ </trans-unit> <trans-unit id="No history" xml:space="preserve"> <source>No history</source> + <target>无历史记录</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="No permission to record voice message" xml:space="preserve"> @@ -3226,6 +3349,10 @@ <target>只有您的联系人可以发送语音消息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Open" xml:space="preserve"> + <source>Open</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Open Settings" xml:space="preserve"> <source>Open Settings</source> <target>打开设置</target> @@ -3316,10 +3443,10 @@ <target>粘贴收到的链接</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Paste the link you received into the box below to connect with your contact." xml:space="preserve"> - <source>Paste the link you received into the box below to connect with your contact.</source> + <trans-unit id="Paste the link you received to connect with your contact." xml:space="preserve"> + <source>Paste the link you received to connect with your contact.</source> <target>将您收到的链接粘贴到下面的框中以与您的联系人联系。</target> - <note>No comment provided by engineer.</note> + <note>placeholder</note> </trans-unit> <trans-unit id="People can connect to you only via the links you share." xml:space="preserve"> <source>People can connect to you only via the links you share.</source> @@ -3518,6 +3645,7 @@ </trans-unit> <trans-unit id="Protocol timeout per KB" xml:space="preserve"> <source>Protocol timeout per KB</source> + <target>每 KB 协议超时</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Push notifications" xml:space="preserve"> @@ -3532,6 +3660,7 @@ </trans-unit> <trans-unit id="React…" xml:space="preserve"> <source>React…</source> + <target>回应…</target> <note>chat item menu</note> </trans-unit> <trans-unit id="Read" xml:space="preserve"> @@ -3564,6 +3693,11 @@ <target>在我们的 [GitHub 仓库](https://github.com/simplex-chat/simplex-chat#readme) 中阅读更多信息。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Receipts are disabled" xml:space="preserve"> + <source>Receipts are disabled</source> + <target>回执已禁用</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Received at" xml:space="preserve"> <source>Received at</source> <target>已收到于</target> @@ -3606,10 +3740,12 @@ </trans-unit> <trans-unit id="Reconnect all connected servers to force message delivery. It uses additional traffic." xml:space="preserve"> <source>Reconnect all connected servers to force message delivery. It uses additional traffic.</source> + <target>重新连接所有已连接的服务器以强制发送信息。这会耗费更多流量。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reconnect servers?" xml:space="preserve"> <source>Reconnect servers?</source> + <target>是否重新连接服务器?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Record updated at" xml:space="preserve"> @@ -3632,8 +3768,8 @@ <target>拒绝</target> <note>reject incoming call via notification</note> </trans-unit> - <trans-unit id="Reject contact (sender NOT notified)" xml:space="preserve"> - <source>Reject contact (sender NOT notified)</source> + <trans-unit id="Reject (sender NOT notified)" xml:space="preserve"> + <source>Reject (sender NOT notified)</source> <target>拒绝联系人(发送者不会被通知)</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -3674,14 +3810,17 @@ </trans-unit> <trans-unit id="Renegotiate" xml:space="preserve"> <source>Renegotiate</source> + <target>重新协商</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption" xml:space="preserve"> <source>Renegotiate encryption</source> + <target>重新协商加密</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Renegotiate encryption?" xml:space="preserve"> <source>Renegotiate encryption?</source> + <target>重新协商加密?</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Reply" xml:space="preserve"> @@ -3941,6 +4080,7 @@ </trans-unit> <trans-unit id="Send delivery receipts to" xml:space="preserve"> <source>Send delivery receipts to</source> + <target>将送达回执发送给</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send direct message" xml:space="preserve"> @@ -3948,6 +4088,10 @@ <target>发送私信</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Send direct message to connect" xml:space="preserve"> + <source>Send direct message to connect</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Send disappearing message" xml:space="preserve"> <source>Send disappearing message</source> <target>发送限时消息中</target> @@ -3980,6 +4124,7 @@ </trans-unit> <trans-unit id="Send receipts" xml:space="preserve"> <source>Send receipts</source> + <target>发送回执</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Send them from gallery or custom keyboards." xml:space="preserve"> @@ -3999,10 +4144,12 @@ </trans-unit> <trans-unit id="Sending delivery receipts will be enabled for all contacts in all visible chat profiles." xml:space="preserve"> <source>Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</source> + <target>将对所有可见聊天配置文件中的所有联系人启用送达回执功能。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending delivery receipts will be enabled for all contacts." xml:space="preserve"> <source>Sending delivery receipts will be enabled for all contacts.</source> + <target>将对所有联系人启用送达回执功能。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending file will be stopped." xml:space="preserve"> @@ -4012,10 +4159,22 @@ </trans-unit> <trans-unit id="Sending receipts is disabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is disabled for %lld contacts</source> + <target>已为 %lld 联系人禁用送达回执功能</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is disabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is disabled for %lld groups</source> + <target>已为 %lld 组禁用送达回执功能</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending receipts is enabled for %lld contacts" xml:space="preserve"> <source>Sending receipts is enabled for %lld contacts</source> + <target>已为 %lld 联系人启用送达回执功能</target> + <note>No comment provided by engineer.</note> + </trans-unit> + <trans-unit id="Sending receipts is enabled for %lld groups" xml:space="preserve"> + <source>Sending receipts is enabled for %lld groups</source> + <target>已为 %lld 组启用送达回执功能</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="Sending via" xml:space="preserve"> @@ -4158,6 +4317,11 @@ <target>显示开发者选项</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Show last messages" xml:space="preserve"> + <source>Show last messages</source> + <target>显示最近的消息</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Show preview" xml:space="preserve"> <source>Show preview</source> <target>显示预览</target> @@ -4228,6 +4392,10 @@ <target>SimpleX 一次性邀请</target> <note>simplex link type</note> </trans-unit> + <trans-unit id="Simplified incognito mode" xml:space="preserve"> + <source>Simplified incognito mode</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Skip" xml:space="preserve"> <source>Skip</source> <target>跳过</target> @@ -4238,6 +4406,11 @@ <target>已跳过消息</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Small groups (max 20)" xml:space="preserve"> + <source>Small groups (max 20)</source> + <target>小群组(最多 20 人)</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Some non-fatal errors occurred during import - you may see Chat console for more details." xml:space="preserve"> <source>Some non-fatal errors occurred during import - you may see Chat console for more details.</source> <target>导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。</target> @@ -4457,6 +4630,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="The encryption is working and the new encryption agreement is not required. It may result in connection errors!" xml:space="preserve"> <source>The encryption is working and the new encryption agreement is not required. It may result in connection errors!</source> + <target>加密正在运行,不需要新的加密协议。这可能会导致连接错误!</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The group is fully decentralized – it is visible only to the members." xml:space="preserve"> @@ -4496,6 +4670,7 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="The second tick we missed! ✅" xml:space="preserve"> <source>The second tick we missed! ✅</source> + <target>我们错过的第二个"√"!✅</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="The sender will NOT be notified" xml:space="preserve"> @@ -4525,10 +4700,12 @@ It can happen because of some bug or when the connection is compromised.</source </trans-unit> <trans-unit id="These settings are for your current profile **%@**." xml:space="preserve"> <source>These settings are for your current profile **%@**.</source> + <target>这些设置适用于您当前的配置文件 **%@**。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="They can be overridden in contact settings" xml:space="preserve"> - <source>They can be overridden in contact settings</source> + <trans-unit id="They can be overridden in contact and group settings." xml:space="preserve"> + <source>They can be overridden in contact and group settings.</source> + <target>可以在联系人和群组设置中覆盖它们。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." xml:space="preserve"> @@ -4546,6 +4723,11 @@ It can happen because of some bug or when the connection is compromised.</source <target>此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="This group has over %lld members, delivery receipts are not sent." xml:space="preserve"> + <source>This group has over %lld members, delivery receipts are not sent.</source> + <target>该组有超过 %lld 个成员,不发送送货单。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="This group no longer exists." xml:space="preserve"> <source>This group no longer exists.</source> <target>该群组已不存在。</target> @@ -4566,11 +4748,6 @@ It can happen because of some bug or when the connection is compromised.</source <target>您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="To find the profile used for an incognito connection, tap the contact or group name on top of the chat." xml:space="preserve"> - <source>To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</source> - <target>要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="To make a new connection" xml:space="preserve"> <source>To make a new connection</source> <target>建立新连接</target> @@ -4613,6 +4790,10 @@ You will be prompted to complete authentication before this feature is enabled.< <target>要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Toggle incognito when connecting." xml:space="preserve"> + <source>Toggle incognito when connecting.</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Transport isolation" xml:space="preserve"> <source>Transport isolation</source> <target>传输隔离</target> @@ -4651,7 +4832,7 @@ You will be prompted to complete authentication before this feature is enabled.< <trans-unit id="Unexpected error: %@" xml:space="preserve"> <source>Unexpected error: %@</source> <target>意外错误: %@</target> - <note>No comment provided by engineer.</note> + <note>item status description</note> </trans-unit> <trans-unit id="Unexpected migration state" xml:space="preserve"> <source>Unexpected migration state</source> @@ -4790,6 +4971,11 @@ To connect, please ask your contact to create another connection link and check <target>使用聊天</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use current profile" xml:space="preserve"> + <source>Use current profile</source> + <target>使用当前配置文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use for new connections" xml:space="preserve"> <source>Use for new connections</source> <target>用于新连接</target> @@ -4800,6 +4986,11 @@ To connect, please ask your contact to create another connection link and check <target>使用 iOS 通话界面</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Use new incognito profile" xml:space="preserve"> + <source>Use new incognito profile</source> + <target>使用新的隐身配置文件</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Use server" xml:space="preserve"> <source>Use server</source> <target>使用服务器</target> @@ -5012,10 +5203,12 @@ To connect, please ask your contact to create another connection link and check </trans-unit> <trans-unit id="You can enable later via Settings" xml:space="preserve"> <source>You can enable later via Settings</source> + <target>您可以稍后在设置中启用它</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can enable them later via app Privacy & Security settings." xml:space="preserve"> <source>You can enable them later via app Privacy & Security settings.</source> + <target>您可以稍后通过应用程序的 "隐私与安全 "设置启用它们。</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="You can hide or mute a user profile - swipe it to the right." xml:space="preserve"> @@ -5088,8 +5281,8 @@ To connect, please ask your contact to create another connection link and check <target>您必须在每次应用程序启动时输入密码——它不存储在设备上。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="You invited your contact" xml:space="preserve"> - <source>You invited your contact</source> + <trans-unit id="You invited a contact" xml:space="preserve"> + <source>You invited a contact</source> <target>您邀请了您的联系人</target> <note>No comment provided by engineer.</note> </trans-unit> @@ -5218,11 +5411,6 @@ To connect, please ask your contact to create another connection link and check <target>您的聊天资料将被发送给群组成员</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your chat profile will be sent to your contact" xml:space="preserve"> - <source>Your chat profile will be sent to your contact</source> - <target>您的聊天资料将被发送给您的联系人</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your chat profiles" xml:space="preserve"> <source>Your chat profiles</source> <target>您的聊天资料</target> @@ -5277,6 +5465,11 @@ You can change it in Settings.</source> <target>您的隐私设置</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="Your profile **%@** will be shared." xml:space="preserve"> + <source>Your profile **%@** will be shared.</source> + <target>您的个人资料 **%@** 将被共享。</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile." xml:space="preserve"> <source>Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.</source> @@ -5284,11 +5477,6 @@ SimpleX servers cannot see your profile.</source> SimpleX 服务器无法看到您的资料。</target> <note>No comment provided by engineer.</note> </trans-unit> - <trans-unit id="Your profile will be sent to the contact that you received this link from" xml:space="preserve"> - <source>Your profile will be sent to the contact that you received this link from</source> - <target>您的个人资料将发送给您收到此链接的联系人</target> - <note>No comment provided by engineer.</note> - </trans-unit> <trans-unit id="Your profile, contacts and delivered messages are stored on your device." xml:space="preserve"> <source>Your profile, contacts and delivered messages are stored on your device.</source> <target>您的资料、联系人和发送的消息存储在您的设备上。</target> @@ -5356,10 +5544,12 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="agreeing encryption for %@…" xml:space="preserve"> <source>agreeing encryption for %@…</source> + <target>正在协商将加密应用于 %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="agreeing encryption…" xml:space="preserve"> <source>agreeing encryption…</source> + <target>同意加密…</target> <note>chat item text</note> </trans-unit> <trans-unit id="always" xml:space="preserve"> @@ -5424,10 +5614,12 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="changing address for %@…" xml:space="preserve"> <source>changing address for %@…</source> + <target>正在将变更的地址应用于 %@…</target> <note>chat item text</note> </trans-unit> <trans-unit id="changing address…" xml:space="preserve"> <source>changing address…</source> + <target>更改地址…</target> <note>chat item text</note> </trans-unit> <trans-unit id="colored" xml:space="preserve"> @@ -5450,6 +5642,10 @@ SimpleX 服务器无法看到您的资料。</target> <target>已连接</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="connected directly" xml:space="preserve"> + <source>connected directly</source> + <note>rcv group event chat item</note> + </trans-unit> <trans-unit id="connecting" xml:space="preserve"> <source>connecting</source> <target>连接中</target> @@ -5532,10 +5728,12 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="default (no)" xml:space="preserve"> <source>default (no)</source> + <target>默认(否)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="default (yes)" xml:space="preserve"> <source>default (yes)</source> + <target>默认 (是)</target> <note>No comment provided by engineer.</note> </trans-unit> <trans-unit id="deleted" xml:space="preserve"> @@ -5558,6 +5756,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>直接</target> <note>connection level description</note> </trans-unit> + <trans-unit id="disabled" xml:space="preserve"> + <source>disabled</source> + <target>关闭</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="duplicate message" xml:space="preserve"> <source>duplicate message</source> <target>重复的消息</target> @@ -5585,34 +5788,42 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="encryption agreed" xml:space="preserve"> <source>encryption agreed</source> + <target>已同意加密</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption agreed for %@" xml:space="preserve"> <source>encryption agreed for %@</source> + <target>同意对 %@ 进行加密</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok" xml:space="preserve"> <source>encryption ok</source> + <target>可以加密</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption ok for %@" xml:space="preserve"> <source>encryption ok for %@</source> + <target>对 %@ 进行加密</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed" xml:space="preserve"> <source>encryption re-negotiation allowed</source> + <target>允许重新进行加密协商</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation allowed for %@" xml:space="preserve"> <source>encryption re-negotiation allowed for %@</source> + <target>允许对 %@ 进行加密重新协商</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required" xml:space="preserve"> <source>encryption re-negotiation required</source> + <target>需要重新进行加密协商</target> <note>chat item text</note> </trans-unit> <trans-unit id="encryption re-negotiation required for %@" xml:space="preserve"> <source>encryption re-negotiation required for %@</source> + <target>需要为 %@ 重新进行加密协商</target> <note>chat item text</note> </trans-unit> <trans-unit id="ended" xml:space="preserve"> @@ -5630,6 +5841,11 @@ SimpleX 服务器无法看到您的资料。</target> <target>错误</target> <note>No comment provided by engineer.</note> </trans-unit> + <trans-unit id="event happened" xml:space="preserve"> + <source>event happened</source> + <target>发生的事</target> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="group deleted" xml:space="preserve"> <source>group deleted</source> <target>群组已删除</target> @@ -5888,8 +6104,13 @@ SimpleX 服务器无法看到您的资料。</target> </trans-unit> <trans-unit id="security code changed" xml:space="preserve"> <source>security code changed</source> + <target>安全密码已更改</target> <note>chat item text</note> </trans-unit> + <trans-unit id="send direct message" xml:space="preserve"> + <source>send direct message</source> + <note>No comment provided by engineer.</note> + </trans-unit> <trans-unit id="starting…" xml:space="preserve"> <source>starting…</source> <target>启动中……</target> @@ -6034,7 +6255,7 @@ SimpleX 服务器无法看到您的资料。</target> </file> <file original="en.lproj/SimpleX--iOS--InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleName" xml:space="preserve"> @@ -6066,7 +6287,7 @@ SimpleX 服务器无法看到您的资料。</target> </file> <file original="SimpleX NSE/en.lproj/InfoPlist.strings" source-language="en" target-language="zh-Hans" datatype="plaintext"> <header> - <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3.1" build-num="14E300c"/> + <tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/> </header> <body> <trans-unit id="CFBundleDisplayName" xml:space="preserve"> diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json index 7d8b8579fe..807a15f96c 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "zh-Hans", "toolInfo" : { - "toolBuildNumber" : "14E300c", + "toolBuildNumber" : "15A240d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "14.3.1" + "toolVersion" : "15.0" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 43ba3ab323..645fdb5952 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -127,7 +127,7 @@ class NotificationService: UNNotificationServiceExtension { logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfMsgInfo), privacy: .public)") if let connEntity = ntfMsgInfo.connEntity { setBestAttemptNtf( - ntfMsgInfo.user.showNotifications + ntfMsgInfo.ntfsEnabled ? .nse(notification: createConnectionEventNtf(ntfMsgInfo.user, connEntity)) : .empty ) @@ -219,7 +219,6 @@ func startChat() -> DBMigrationResult? { let justStarted = try apiStartChat() chatStarted = true if justStarted { - try apiSetIncognito(incognito: incognitoGroupDefault.get()) chatLastStartGroupDefault.set(Date.now) Task { await receiveMessages() } } @@ -272,7 +271,7 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? { ntfBadgeCountGroupDefault.set(max(0, ntfBadgeCountGroupDefault.get() - 1)) } if let file = cItem.autoReceiveFile() { - cItem = autoReceiveFile(file) ?? cItem + cItem = autoReceiveFile(file, encrypted: cItem.encryptLocalFile) ?? cItem } let ntf: NSENotification = cInfo.ntfsEnabled ? .nse(notification: createMessageReceivedNtf(user, cInfo, cItem)) : .empty return cItem.showNotification ? (aChatItem.chatId, ntf) : nil @@ -352,12 +351,6 @@ func setXFTPConfig(_ cfg: XFTPFileConfig?) throws { throw r } -func apiSetIncognito(incognito: Bool) throws { - let r = sendSimpleXCmd(.setIncognito(incognito: incognito)) - if case .cmdOk = r { return } - throw r -} - func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { guard apiGetActiveUser() != nil else { logger.debug("no active user") @@ -374,25 +367,25 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { return nil } -func apiReceiveFile(fileId: Int64, inline: Bool? = nil) -> AChatItem? { - let r = sendSimpleXCmd(.receiveFile(fileId: fileId, inline: inline)) +func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { + let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } logger.error("receiveFile error: \(responseError(r))") return nil } -func apiSetFileToReceive(fileId: Int64) { - let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId)) +func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { + let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted)) if case .cmdOk = r { return } logger.error("setFileToReceive error: \(responseError(r))") } -func autoReceiveFile(_ file: CIFile) -> ChatItem? { +func autoReceiveFile(_ file: CIFile, encrypted: Bool) -> ChatItem? { switch file.fileProtocol { case .smp: - return apiReceiveFile(fileId: file.fileId)?.chatItem + return apiReceiveFile(fileId: file.fileId, encrypted: encrypted)?.chatItem case .xftp: - apiSetFileToReceive(fileId: file.fileId) + apiSetFileToReceive(fileId: file.fileId, encrypted: encrypted) return nil } } @@ -408,4 +401,8 @@ struct NtfMessages { var connEntity: ConnectionEntity? var msgTs: Date? var ntfMessages: [NtfMsgInfo] + + var ntfsEnabled: Bool { + user.showNotifications && (connEntity?.ntfsEnabled ?? false) + } } diff --git a/apps/ios/SimpleX NSE/bg.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/bg.lproj/InfoPlist.strings new file mode 100644 index 0000000000..b1c515fbb4 --- /dev/null +++ b/apps/ios/SimpleX NSE/bg.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Авторско право © 2022 SimpleX Chat. Всички права запазени."; + diff --git a/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/en.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings new file mode 100644 index 0000000000..28a503d909 --- /dev/null +++ b/apps/ios/SimpleX NSE/fi.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. Kaikki oikeudet pidätetään."; + diff --git a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX NSE/th.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/th.lproj/InfoPlist.strings new file mode 100644 index 0000000000..2d0d7428f0 --- /dev/null +++ b/apps/ios/SimpleX NSE/th.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "ลิขสิทธิ์ © 2022 SimpleX Chat สงวนลิขสิทธิ์"; + diff --git a/apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings new file mode 100644 index 0000000000..da1f5367b3 --- /dev/null +++ b/apps/ios/SimpleX NSE/uk.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Авторське право © 2022 SimpleX Chat. Всі права захищені."; + diff --git a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index 8b13789179..0000000000 --- a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index b39c165732..20ba15e925 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -48,6 +48,11 @@ 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; }; 5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; }; + 5C56251A2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */; }; + 5C56251B2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */; }; + 5C56251C2AC1DE5900A21210 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625172AC1DE5900A21210 /* libgmpxx.a */; }; + 5C56251D2AC1DE5900A21210 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625182AC1DE5900A21210 /* libgmp.a */; }; + 5C56251E2AC1DE5900A21210 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5625192AC1DE5900A21210 /* libffi.a */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; 5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */; }; 5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */; }; @@ -77,6 +82,7 @@ 5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */; }; 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; + 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; @@ -84,15 +90,9 @@ 5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; }; 5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; }; 5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; }; - 5CA89A442A88020700021BE9 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA89A3F2A88020700021BE9 /* libffi.a */; }; - 5CA89A452A88020700021BE9 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA89A402A88020700021BE9 /* libgmp.a */; }; - 5CA89A462A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA89A412A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6.a */; }; - 5CA89A472A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA89A422A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6-ghc8.10.7.a */; }; - 5CA89A482A88020700021BE9 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA89A432A88020700021BE9 /* libgmpxx.a */; }; 5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; }; 5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; }; 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; }; - 5CB0BA8B2826CB3A00B3292C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA892826CB3A00B3292C /* Localizable.strings */; }; 5CB0BA8E2827126500B3292C /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8D2827126500B3292C /* OnboardingView.swift */; }; 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */; }; 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB0BA91282713FD00B3292C /* CreateProfile.swift */; }; @@ -153,6 +153,8 @@ 5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; }; 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; + 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; + 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; @@ -268,6 +270,8 @@ 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; }; 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = "<group>"; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; }; + 5C136D8E2AAB3D14006DE2FC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = "fi.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + 5C136D8F2AAB3D14006DE2FC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; }; 5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemView.swift; sourceTree = "<group>"; }; @@ -289,19 +293,27 @@ 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = "<group>"; }; 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; }; 5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = "<group>"; }; + 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a"; sourceTree = "<group>"; }; + 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a"; sourceTree = "<group>"; }; + 5C5625172AC1DE5900A21210 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; }; + 5C5625182AC1DE5900A21210 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; + 5C5625192AC1DE5900A21210 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; }; 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = "<group>"; }; 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatFeatureView.swift; sourceTree = "<group>"; }; + 5C5B67912ABAF4B500DA9412 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; }; + 5C5B67922ABAF56000DA9412 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = "bg.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + 5C5B67932ABAF56000DA9412 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettings.swift; sourceTree = "<group>"; }; 5C5E5D3A2824468B00B0488A /* ActiveCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveCallView.swift; sourceTree = "<group>"; }; 5C5E5D3C282447AB00B0488A /* CallTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallTypes.swift; sourceTree = "<group>"; }; 5C5F2B6C27EBC3FE006A9D5F /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; }; 5C5F2B6F27EBC704006A9D5F /* ProfileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileImage.swift; sourceTree = "<group>"; }; + 5C636F662AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = "uk.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + 5C636F672AAB3D2400751C84 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C65DAE429C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; - 5C65DAE529C77136003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; }; 5C65DAE629C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C65DAE729C771B9003CEE45 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 5C65DAEA29CB8867003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5C65DAEB29CB8867003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C65DAEC29CB8908003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = "es.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C65DAED29CB8908003CEE45 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C65DAF829D0CC20003CEE45 /* DeveloperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperView.swift; sourceTree = "<group>"; }; @@ -316,11 +328,9 @@ 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = "<group>"; }; 5C764E88279CBCB3000C6508 /* ChatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModel.swift; sourceTree = "<group>"; }; 5C84FE9129A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5C84FE9229A216C800D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C84FE9329A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = "nl.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C84FE9429A2179C00D95B1A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C8B41C929AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5C8B41CA29AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = "<group>"; }; @@ -335,8 +345,8 @@ 5C9C2DA82899DA6F00CC63B1 /* NetworkAndServers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAndServers.swift; sourceTree = "<group>"; }; 5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseErrorView.swift; sourceTree = "<group>"; }; 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; }; - 5C9CC7B128D1F8F400BEF955 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; }; + 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; }; @@ -346,26 +356,20 @@ 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = "<group>"; }; 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOSLaunchTests.swift; sourceTree = "<group>"; }; + 5CA3ED4D2A942170005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; }; + 5CA3ED4F2A9422D1005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = "th.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; + 5CA3ED502A9422D1005D71E2 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSheet.swift; sourceTree = "<group>"; }; 5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5CA85D0B297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CA85D0C297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = "it.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CA85D0D297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; - 5CA89A3F2A88020700021BE9 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; }; - 5CA89A402A88020700021BE9 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; }; - 5CA89A412A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6.a"; sourceTree = "<group>"; }; - 5CA89A422A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6-ghc8.10.7.a"; sourceTree = "<group>"; }; - 5CA89A432A88020700021BE9 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; }; 5CAB912529E93F9400F34A95 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5CAB912629E93F9400F34A95 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CAC41182A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5CAC41192A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CAC411A2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CAC411B2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5CADE79929211BB900072E13 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; }; 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPreferencesView.swift; sourceTree = "<group>"; }; 5CB0BA872826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; }; - 5CB0BA8A2826CB3A00B3292C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CB0BA8D2827126500B3292C /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; }; 5CB0BA8F282713D900B3292C /* SimpleXInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXInfo.swift; sourceTree = "<group>"; }; 5CB0BA91282713FD00B3292C /* CreateProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateProfile.swift; sourceTree = "<group>"; }; @@ -374,7 +378,6 @@ 5CB2085028DB64CA00D024EC /* CreateLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLinkView.swift; sourceTree = "<group>"; }; 5CB2085228DB7CAF00D024EC /* ConnectViaLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectViaLinkView.swift; sourceTree = "<group>"; }; 5CB2085428DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5CB2085528DE647400D024EC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; }; 5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; }; 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = "<group>"; }; @@ -386,7 +389,6 @@ 5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; }; 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListNavLink.swift; sourceTree = "<group>"; }; 5CBD285529565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; }; - 5CBD285629565CAE00EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CBD285729565D2600EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = "fr.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; }; 5CBD285829565D2600EC2CF4 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUtils.swift; sourceTree = "<group>"; }; @@ -422,6 +424,8 @@ 5CE2BA96284537A800EC33A6 /* dummy.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = dummy.m; sourceTree = "<group>"; }; 5CE4407127ADB1D0007B033A /* Emoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Emoji.swift; sourceTree = "<group>"; }; 5CE4407827ADB701007B033A /* EmojiItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiItemView.swift; sourceTree = "<group>"; }; + 5CE6C7B32AAB1515007F345C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; }; + 5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = "<group>"; }; 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = "<group>"; }; @@ -430,6 +434,8 @@ 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; }; 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; }; + 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; }; + 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; }; 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; }; @@ -501,13 +507,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CA89A472A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6-ghc8.10.7.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5CA89A442A88020700021BE9 /* libffi.a in Frameworks */, - 5CA89A452A88020700021BE9 /* libgmp.a in Frameworks */, - 5CA89A462A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6.a in Frameworks */, + 5C56251C2AC1DE5900A21210 /* libgmpxx.a in Frameworks */, + 5C56251B2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a in Frameworks */, + 5C56251A2AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a in Frameworks */, + 5C56251E2AC1DE5900A21210 /* libffi.a in Frameworks */, + 5C56251D2AC1DE5900A21210 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5CA89A482A88020700021BE9 /* libgmpxx.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -568,11 +574,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5CA89A3F2A88020700021BE9 /* libffi.a */, - 5CA89A402A88020700021BE9 /* libgmp.a */, - 5CA89A432A88020700021BE9 /* libgmpxx.a */, - 5CA89A422A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6-ghc8.10.7.a */, - 5CA89A412A88020700021BE9 /* libHSsimplex-chat-5.2.3.0-B6vDAdCeaTjEONCeTB2nA6.a */, + 5C5625192AC1DE5900A21210 /* libffi.a */, + 5C5625182AC1DE5900A21210 /* libgmp.a */, + 5C5625172AC1DE5900A21210 /* libgmpxx.a */, + 5C5625152AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7-ghc8.10.7.a */, + 5C5625162AC1DE5900A21210 /* libHSsimplex-chat-5.3.1.0-625aldG8rLm27VEosiv5y7.a */, ); path = Libraries; sourceTree = "<group>"; @@ -732,10 +738,10 @@ 5CADE79929211BB900072E13 /* PreferencesView.swift */, 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */, 5C05DF522840AA1D00C683F9 /* CallSettings.swift */, - 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, 5CB924E027A867BA00ACCCDD /* UserProfile.swift */, 5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */, 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */, + 5C3F1D57284363C400EC8A82 /* PrivacySettings.swift */, 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */, 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */, 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */, @@ -772,7 +778,6 @@ 5CDCAD5128186DE400503DA2 /* SimpleX NSE.entitlements */, 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, - 5CB0BA892826CB3A00B3292C /* Localizable.strings */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, ); path = "SimpleX NSE"; @@ -789,6 +794,7 @@ 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, + 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */, 5C00168028C4FE760094D739 /* KeyChain.swift */, 5CE2BA76284530BF00EC33A6 /* SimpleXChat.h */, 5CE2BA8A2845332200EC33A6 /* SimpleX.h */, @@ -823,6 +829,7 @@ 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */, 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */, 5CC868F229EB540C0017BBFD /* CIRcvDecryptionError.swift */, + 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */, ); path = ChatItem; sourceTree = "<group>"; @@ -838,6 +845,7 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */, 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */, D72A9087294BD7A70047C86D /* NativeTextEditor.swift */, + 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */, ); path = ComposeMessage; sourceTree = "<group>"; @@ -1012,6 +1020,10 @@ es, pl, ja, + th, + fi, + uk, + bg, ); mainGroup = 5CA059BD279559F40002BEB4; packageReferences = ( @@ -1057,7 +1069,6 @@ buildActionMask = 2147483647; files = ( 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */, - 5CB0BA8B2826CB3A00B3292C /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1093,6 +1104,7 @@ 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */, 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */, + 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, 5C029EAA283942EA004A9677 /* CallController.swift in Sources */, @@ -1154,6 +1166,7 @@ 5C2E260F27A30FDC00F70299 /* ChatView.swift in Sources */, 5C2E260B27A30CFA00F70299 /* ChatListView.swift in Sources */, 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */, + 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */, 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */, 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */, 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, @@ -1246,6 +1259,7 @@ 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */, 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */, + 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */, 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */, 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, @@ -1292,28 +1306,14 @@ 5C65DAED29CB8908003CEE45 /* es */, 5C6D183329E93FBA00D430B3 /* pl */, 5CAC411B2A192DE800C331A2 /* ja */, + 5CA3ED502A9422D1005D71E2 /* th */, + 5C136D8F2AAB3D14006DE2FC /* fi */, + 5C636F672AAB3D2400751C84 /* uk */, + 5C5B67932ABAF56000DA9412 /* bg */, ); name = InfoPlist.strings; sourceTree = "<group>"; }; - 5CB0BA892826CB3A00B3292C /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - 5CB0BA8A2826CB3A00B3292C /* ru */, - 5C9CC7B128D1F8F400BEF955 /* en */, - 5CB2085528DE647400D024EC /* de */, - 5CBD285629565CAE00EC2CF4 /* fr */, - 5CA85D0B297218AA0095AF72 /* it */, - 5C84FE9229A216C800D95B1A /* nl */, - 5C8B41CA29AF41BC00888272 /* cs */, - 5C65DAE529C77136003CEE45 /* zh-Hans */, - 5C65DAEB29CB8867003CEE45 /* es */, - 5CAB912629E93F9400F34A95 /* pl */, - 5CAC41192A192D8400C331A2 /* ja */, - ); - name = Localizable.strings; - sourceTree = "<group>"; - }; 5CC2C0FA2809BF11000C35E3 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -1328,6 +1328,10 @@ 5C65DAEA29CB8867003CEE45 /* es */, 5CAB912529E93F9400F34A95 /* pl */, 5CAC41182A192D8400C331A2 /* ja */, + 5CA3ED4D2A942170005D71E2 /* th */, + 5CE6C7B32AAB1515007F345C /* fi */, + 5CE6C7B42AAB1527007F345C /* uk */, + 5C5B67912ABAF4B500DA9412 /* bg */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -1345,6 +1349,10 @@ 5C65DAEC29CB8908003CEE45 /* es */, 5C6D183229E93FBA00D430B3 /* pl */, 5CAC411A2A192DE800C331A2 /* ja */, + 5CA3ED4F2A9422D1005D71E2 /* th */, + 5C136D8E2AAB3D14006DE2FC /* fi */, + 5C636F662AAB3D2400751C84 /* uk */, + 5C5B67922ABAF56000DA9412 /* bg */, ); name = "SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; @@ -1478,7 +1486,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 166; + CURRENT_PROJECT_VERSION = 174; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1499,7 +1507,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.2.3; + MARKETING_VERSION = 5.3.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1520,7 +1528,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 166; + CURRENT_PROJECT_VERSION = 174; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1541,7 +1549,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 5.2.3; + MARKETING_VERSION = 5.3.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1600,7 +1608,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 166; + CURRENT_PROJECT_VERSION = 174; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1613,7 +1621,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.2.3; + MARKETING_VERSION = 5.3.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1632,7 +1640,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 166; + CURRENT_PROJECT_VERSION = 174; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1645,7 +1653,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 5.2.3; + MARKETING_VERSION = 5.3.1; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1664,7 +1672,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 166; + CURRENT_PROJECT_VERSION = 174; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1688,7 +1696,7 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 5.2.3; + MARKETING_VERSION = 5.3.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1710,7 +1718,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 166; + CURRENT_PROJECT_VERSION = 174; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1734,7 +1742,7 @@ "$(inherited)", "$(PROJECT_DIR)/Libraries/sim", ); - MARKETING_VERSION = 5.2.3; + MARKETING_VERSION = 5.3.1; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index a26d7bb41d..e3d202c124 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -136,7 +136,7 @@ public func chatResponse(_ s: String) -> ChatResponse { type = jResp.allKeys[0] as? String if type == "apiChats" { if let jApiChats = jResp["apiChats"] as? NSDictionary, - let user: User = try? decodeObject(jApiChats["user"] as Any), + let user: UserRef = try? decodeObject(jApiChats["user"] as Any), let jChats = jApiChats["chats"] as? NSArray { let chats = jChats.map { jChat in if let chatData = try? parseChatData(jChat) { @@ -148,16 +148,21 @@ public func chatResponse(_ s: String) -> ChatResponse { } } else if type == "apiChat" { if let jApiChat = jResp["apiChat"] as? NSDictionary, - let user: User = try? decodeObject(jApiChat["user"] as Any), + let user: UserRef = try? decodeObject(jApiChat["user"] as Any), let jChat = jApiChat["chat"] as? NSDictionary, let chat = try? parseChatData(jChat) { return .apiChat(user: user, chat: chat) } } else if type == "chatCmdError" { if let jError = jResp["chatCmdError"] as? NSDictionary { - let user: User? = try? decodeObject(jError["user_"] as Any) + let user: UserRef? = try? decodeObject(jError["user_"] as Any) return .chatCmdError(user_: user, chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) } + } else if type == "chatError" { + if let jError = jResp["chatError"] as? NSDictionary { + let user: UserRef? = try? decodeObject(jError["user_"] as Any) + return .chatError(user_: user, chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) + } } } json = prettyJSON(j) @@ -206,7 +211,7 @@ public func responseError(_ err: Error) -> String { switch r { case let .chatCmdError(_, chatError): return chatErrorString(chatError) case let .chatError(_, chatError): return chatErrorString(chatError) - default: return String(describing: r) + default: return "\(String(describing: r.responseType)), details: \(String(describing: r.details))" } } else { return String(describing: err) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 3bc918efe2..b0834f5715 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI -let jsonDecoder = getJSONDecoder() +public let jsonDecoder = getJSONDecoder() let jsonEncoder = getJSONEncoder() public enum ChatCommand { @@ -19,6 +19,7 @@ public enum ChatCommand { case apiSetActiveUser(userId: Int64, viewPwd: String?) case setAllContactReceipts(enable: Bool) case apiSetUserContactReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) + case apiSetUserGroupReceipts(userId: Int64, userMsgReceiptSettings: UserMsgReceiptSettings) case apiHideUser(userId: Int64, viewPwd: String) case apiUnhideUser(userId: Int64, viewPwd: String) case apiMuteUser(userId: Int64) @@ -31,7 +32,6 @@ public enum ChatCommand { case setTempFolder(tempFolder: String) case setFilesFolder(filesFolder: String) case apiSetXFTPConfig(config: XFTPFileConfig?) - case setIncognito(incognito: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) case apiDeleteStorage @@ -39,7 +39,7 @@ public enum ChatCommand { case apiGetChats(userId: Int64) case apiGetChat(type: ChatType, id: Int64, pagination: ChatPagination, search: String) case apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) - case apiSendMessage(type: ChatType, id: Int64, file: String?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?) + case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) @@ -61,6 +61,8 @@ public enum ChatCommand { case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) case apiDeleteGroupLink(groupId: Int64) case apiGetGroupLink(groupId: Int64) + case apiCreateMemberContact(groupId: Int64, groupMemberId: Int64) + case apiSendMemberContactInvitation(contactId: Int64, msg: MsgContent) case apiGetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol) case apiSetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol, servers: [ServerCfg]) case apiTestProtoServer(userId: Int64, server: String) @@ -82,8 +84,9 @@ public enum ChatCommand { case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64) case apiVerifyContact(contactId: Int64, connectionCode: String?) case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?) - case apiAddContact(userId: Int64) - case apiConnect(userId: Int64, connReq: String) + case apiAddContact(userId: Int64, incognito: Bool) + case apiSetConnectionIncognito(connId: Int64, incognito: Bool) + case apiConnect(userId: Int64, incognito: Bool, connReq: String) case apiDeleteChat(type: ChatType, id: Int64) case apiClearChat(type: ChatType, id: Int64) case apiListContacts(userId: Int64) @@ -96,7 +99,7 @@ public enum ChatCommand { case apiShowMyAddress(userId: Int64) case apiSetProfileAddress(userId: Int64, on: Bool) case apiAddressAutoAccept(userId: Int64, autoAccept: AutoAccept?) - case apiAcceptContact(contactReqId: Int64) + case apiAcceptContact(incognito: Bool, contactReqId: Int64) case apiRejectContact(contactReqId: Int64) // WebRTC calls case apiSendCallInvitation(contact: Contact, callType: CallType) @@ -109,8 +112,8 @@ public enum ChatCommand { case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) - case receiveFile(fileId: Int64, inline: Bool?) - case setFileToReceive(fileId: Int64) + case receiveFile(fileId: Int64, encrypted: Bool, inline: Bool?) + case setFileToReceive(fileId: Int64, encrypted: Bool) case cancelFile(fileId: Int64) case showVersion case string(String) @@ -127,7 +130,10 @@ public enum ChatCommand { case let .setAllContactReceipts(enable): return "/set receipts all \(onOff(enable))" case let .apiSetUserContactReceipts(userId, userMsgReceiptSettings): let umrs = userMsgReceiptSettings - return "/_set receipts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + return "/_set receipts contacts \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" + case let .apiSetUserGroupReceipts(userId, userMsgReceiptSettings): + let umrs = userMsgReceiptSettings + return "/_set receipts groups \(userId) \(onOff(umrs.enable)) clear_overrides=\(onOff(umrs.clearOverrides))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" case let .apiUnhideUser(userId, viewPwd): return "/_unhide user \(userId) \(encodeJSON(viewPwd))" case let .apiMuteUser(userId): return "/_mute user \(userId)" @@ -144,7 +150,6 @@ public enum ChatCommand { } else { return "/_xftp off" } - case let .setIncognito(incognito): return "/incognito \(onOff(incognito))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" case .apiDeleteStorage: return "/_db delete" @@ -154,7 +159,7 @@ public enum ChatCommand { (search == "" ? "" : " search=\(search)") case let .apiGetChatItemInfo(type, id, itemId): return "/_get item info \(ref(type, id)) \(itemId)" case let .apiSendMessage(type, id, file, quotedItemId, mc, live, ttl): - let msg = encodeJSON(ComposedMessage(filePath: file, quotedItemId: quotedItemId, msgContent: mc)) + let msg = encodeJSON(ComposedMessage(fileSource: file, quotedItemId: quotedItemId, msgContent: mc)) let ttlStr = ttl != nil ? "\(ttl!)" : "default" return "/_send \(ref(type, id)) live=\(onOff(live)) ttl=\(ttlStr) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)" @@ -178,6 +183,8 @@ public enum ChatCommand { case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" + case let .apiCreateMemberContact(groupId, groupMemberId): return "/_create member contact #\(groupId) \(groupMemberId)" + case let .apiSendMemberContactInvitation(contactId, mc): return "/_invite member contact @\(contactId) \(mc.cmdString)" case let .apiGetUserProtoServers(userId, serverProtocol): return "/_servers \(userId) \(serverProtocol)" case let .apiSetUserProtoServers(userId, serverProtocol, servers): return "/_servers \(userId) \(serverProtocol) \(protoServersStr(servers))" case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)" @@ -209,8 +216,9 @@ public enum ChatCommand { case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)" case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)" case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)" - case let .apiAddContact(userId): return "/_connect \(userId)" - case let .apiConnect(userId, connReq): return "/_connect \(userId) \(connReq)" + case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" + case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" + case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)" case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case let .apiListContacts(userId): return "/_contacts \(userId)" @@ -223,7 +231,7 @@ public enum ChatCommand { case let .apiShowMyAddress(userId): return "/_show_address \(userId)" case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiAddressAutoAccept(userId, autoAccept): return "/_auto_accept \(userId) \(AutoAccept.cmdString(autoAccept))" - case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)" + case let .apiAcceptContact(incognito, contactReqId): return "/_accept incognito=\(onOff(incognito)) \(contactReqId)" case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" case let .apiRejectCall(contact): return "/_call reject @\(contact.apiId)" @@ -235,12 +243,13 @@ public enum ChatCommand { case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" - case let .receiveFile(fileId, inline): + case let .receiveFile(fileId, encrypted, inline): + let s = "/freceive \(fileId) encrypt=\(onOff(encrypted))" if let inline = inline { - return "/freceive \(fileId) inline=\(onOff(inline))" + return s + " inline=\(onOff(inline))" } - return "/freceive \(fileId)" - case let .setFileToReceive(fileId): return "/_set_file_to_receive \(fileId)" + return s + case let .setFileToReceive(fileId, encrypted): return "/_set_file_to_receive \(fileId) encrypt=\(onOff(encrypted))" case let .cancelFile(fileId): return "/fcancel \(fileId)" case .showVersion: return "/version" case let .string(str): return str @@ -257,6 +266,7 @@ public enum ChatCommand { case .apiSetActiveUser: return "apiSetActiveUser" case .setAllContactReceipts: return "setAllContactReceipts" case .apiSetUserContactReceipts: return "apiSetUserContactReceipts" + case .apiSetUserGroupReceipts: return "apiSetUserGroupReceipts" case .apiHideUser: return "apiHideUser" case .apiUnhideUser: return "apiUnhideUser" case .apiMuteUser: return "apiMuteUser" @@ -269,7 +279,6 @@ public enum ChatCommand { case .setTempFolder: return "setTempFolder" case .setFilesFolder: return "setFilesFolder" case .apiSetXFTPConfig: return "apiSetXFTPConfig" - case .setIncognito: return "setIncognito" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" case .apiDeleteStorage: return "apiDeleteStorage" @@ -299,6 +308,8 @@ public enum ChatCommand { case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole" case .apiDeleteGroupLink: return "apiDeleteGroupLink" case .apiGetGroupLink: return "apiGetGroupLink" + case .apiCreateMemberContact: return "apiCreateMemberContact" + case .apiSendMemberContactInvitation: return "apiSendMemberContactInvitation" case .apiGetUserProtoServers: return "apiGetUserProtoServers" case .apiSetUserProtoServers: return "apiSetUserProtoServers" case .apiTestProtoServer: return "apiTestProtoServer" @@ -321,6 +332,7 @@ public enum ChatCommand { case .apiVerifyContact: return "apiVerifyContact" case .apiVerifyGroupMember: return "apiVerifyGroupMember" case .apiAddContact: return "apiAddContact" + case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" case .apiConnect: return "apiConnect" case .apiDeleteChat: return "apiDeleteChat" case .apiClearChat: return "apiClearChat" @@ -419,125 +431,131 @@ public enum ChatResponse: Decodable, Error { case chatRunning case chatStopped case chatSuspended - case apiChats(user: User, chats: [ChatData]) - case apiChat(user: User, chat: ChatData) - case chatItemInfo(user: User, chatItem: AChatItem, chatItemInfo: ChatItemInfo) - case userProtoServers(user: User, servers: UserProtoServers) - case serverTestResult(user: User, testServer: String, testFailure: ProtocolTestFailure?) - case chatItemTTL(user: User, chatItemTTL: Int64?) + case apiChats(user: UserRef, chats: [ChatData]) + case apiChat(user: UserRef, chat: ChatData) + case chatItemInfo(user: UserRef, chatItem: AChatItem, chatItemInfo: ChatItemInfo) + case userProtoServers(user: UserRef, servers: UserProtoServers) + case serverTestResult(user: UserRef, testServer: String, testFailure: ProtocolTestFailure?) + case chatItemTTL(user: UserRef, chatItemTTL: Int64?) case networkConfig(networkConfig: NetCfg) - case contactInfo(user: User, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) - case groupMemberInfo(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) - case contactSwitchStarted(user: User, contact: Contact, connectionStats: ConnectionStats) - case groupMemberSwitchStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) - case contactSwitchAborted(user: User, contact: Contact, connectionStats: ConnectionStats) - case groupMemberSwitchAborted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) - case contactSwitch(user: User, contact: Contact, switchProgress: SwitchProgress) - case groupMemberSwitch(user: User, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress) - case contactRatchetSyncStarted(user: User, contact: Contact, connectionStats: ConnectionStats) - case groupMemberRatchetSyncStarted(user: User, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) - case contactRatchetSync(user: User, contact: Contact, ratchetSyncProgress: RatchetSyncProgress) - case groupMemberRatchetSync(user: User, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress) - case contactVerificationReset(user: User, contact: Contact) - case groupMemberVerificationReset(user: User, groupInfo: GroupInfo, member: GroupMember) - case contactCode(user: User, contact: Contact, connectionCode: String) - case groupMemberCode(user: User, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) - case connectionVerified(user: User, verified: Bool, expectedCode: String) - case invitation(user: User, connReqInvitation: String) - case sentConfirmation(user: User) - case sentInvitation(user: User) - case contactAlreadyExists(user: User, contact: Contact) - case contactDeleted(user: User, contact: Contact) - case chatCleared(user: User, chatInfo: ChatInfo) + case contactInfo(user: UserRef, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?) + case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberSwitchAborted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactSwitch(user: UserRef, contact: Contact, switchProgress: SwitchProgress) + case groupMemberSwitch(user: UserRef, groupInfo: GroupInfo, member: GroupMember, switchProgress: SwitchProgress) + case contactRatchetSyncStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) + case groupMemberRatchetSyncStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) + case contactRatchetSync(user: UserRef, contact: Contact, ratchetSyncProgress: RatchetSyncProgress) + case groupMemberRatchetSync(user: UserRef, groupInfo: GroupInfo, member: GroupMember, ratchetSyncProgress: RatchetSyncProgress) + case contactVerificationReset(user: UserRef, contact: Contact) + case groupMemberVerificationReset(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case contactCode(user: UserRef, contact: Contact, connectionCode: String) + case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) + case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) + case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection) + case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) + case sentConfirmation(user: UserRef) + case sentInvitation(user: UserRef) + case contactAlreadyExists(user: UserRef, contact: Contact) + case contactRequestAlreadyAccepted(user: UserRef, contact: Contact) + case contactDeleted(user: UserRef, contact: Contact) + case chatCleared(user: UserRef, chatInfo: ChatInfo) case userProfileNoChange(user: User) - case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile) + case userProfileUpdated(user: User, fromProfile: Profile, toProfile: Profile, updateSummary: UserProfileUpdateSummary) case userPrivacy(user: User, updatedUser: User) - case contactAliasUpdated(user: User, toContact: Contact) - case connectionAliasUpdated(user: User, toConnection: PendingContactConnection) + case contactAliasUpdated(user: UserRef, toContact: Contact) + case connectionAliasUpdated(user: UserRef, toConnection: PendingContactConnection) case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact) case userContactLink(user: User, contactLink: UserContactLink) case userContactLinkUpdated(user: User, contactLink: UserContactLink) case userContactLinkCreated(user: User, connReqContact: String) case userContactLinkDeleted(user: User) - case contactConnected(user: User, contact: Contact, userCustomProfile: Profile?) - case contactConnecting(user: User, contact: Contact) - case receivedContactRequest(user: User, contactRequest: UserContactRequest) - case acceptingContactRequest(user: User, contact: Contact) - case contactRequestRejected(user: User) - case contactUpdated(user: User, toContact: Contact) + case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) + case contactConnecting(user: UserRef, contact: Contact) + case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) + case acceptingContactRequest(user: UserRef, contact: Contact) + case contactRequestRejected(user: UserRef) + case contactUpdated(user: UserRef, toContact: Contact) case contactsSubscribed(server: String, contactRefs: [ContactRef]) case contactsDisconnected(server: String, contactRefs: [ContactRef]) - case contactSubError(user: User, contact: Contact, chatError: ChatError) - case contactSubSummary(user: User, contactSubscriptions: [ContactSubStatus]) - case groupSubscribed(user: User, groupInfo: GroupInfo) - case memberSubErrors(user: User, memberSubErrors: [MemberSubError]) - case groupEmpty(user: User, groupInfo: GroupInfo) + case contactSubError(user: UserRef, contact: Contact, chatError: ChatError) + case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus]) + case groupSubscribed(user: UserRef, groupInfo: GroupInfo) + case memberSubErrors(user: UserRef, memberSubErrors: [MemberSubError]) + case groupEmpty(user: UserRef, groupInfo: GroupInfo) case userContactLinkSubscribed - case newChatItem(user: User, chatItem: AChatItem) - case chatItemStatusUpdated(user: User, chatItem: AChatItem) - case chatItemUpdated(user: User, chatItem: AChatItem) - case chatItemReaction(user: User, added: Bool, reaction: ACIReaction) - case chatItemDeleted(user: User, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool) - case contactsList(user: User, contacts: [Contact]) + case newChatItem(user: UserRef, chatItem: AChatItem) + case chatItemStatusUpdated(user: UserRef, chatItem: AChatItem) + case chatItemUpdated(user: UserRef, chatItem: AChatItem) + case chatItemNotChanged(user: UserRef, chatItem: AChatItem) + case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) + case chatItemDeleted(user: UserRef, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool) + case contactsList(user: UserRef, contacts: [Contact]) // group events - case groupCreated(user: User, groupInfo: GroupInfo) - case sentGroupInvitation(user: User, groupInfo: GroupInfo, contact: Contact, member: GroupMember) - case userAcceptedGroupSent(user: User, groupInfo: GroupInfo, hostContact: Contact?) - case userDeletedMember(user: User, groupInfo: GroupInfo, member: GroupMember) - case leftMemberUser(user: User, groupInfo: GroupInfo) - case groupMembers(user: User, group: Group) - case receivedGroupInvitation(user: User, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) - case groupDeletedUser(user: User, groupInfo: GroupInfo) - case joinedGroupMemberConnecting(user: User, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) - case memberRole(user: User, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case memberRoleUser(user: User, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) - case deletedMemberUser(user: User, groupInfo: GroupInfo, member: GroupMember) - case deletedMember(user: User, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember) - case leftMember(user: User, groupInfo: GroupInfo, member: GroupMember) - case groupDeleted(user: User, groupInfo: GroupInfo, member: GroupMember) - case contactsMerged(user: User, intoContact: Contact, mergedContact: Contact) - case groupInvitation(user: User, groupInfo: GroupInfo) // unused - case userJoinedGroup(user: User, groupInfo: GroupInfo) - case joinedGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember) - case connectedToGroupMember(user: User, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?) - case groupRemoved(user: User, groupInfo: GroupInfo) // unused - case groupUpdated(user: User, toGroup: GroupInfo) - case groupLinkCreated(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) - case groupLink(user: User, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) - case groupLinkDeleted(user: User, groupInfo: GroupInfo) + case groupCreated(user: UserRef, groupInfo: GroupInfo) + case sentGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, member: GroupMember) + case userAcceptedGroupSent(user: UserRef, groupInfo: GroupInfo, hostContact: Contact?) + case userDeletedMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case leftMemberUser(user: UserRef, groupInfo: GroupInfo) + case groupMembers(user: UserRef, group: Group) + case receivedGroupInvitation(user: UserRef, groupInfo: GroupInfo, contact: Contact, memberRole: GroupMemberRole) + case groupDeletedUser(user: UserRef, groupInfo: GroupInfo) + case joinedGroupMemberConnecting(user: UserRef, groupInfo: GroupInfo, hostMember: GroupMember, member: GroupMember) + case memberRole(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) + case memberRoleUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember, fromRole: GroupMemberRole, toRole: GroupMemberRole) + case deletedMemberUser(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case deletedMember(user: UserRef, groupInfo: GroupInfo, byMember: GroupMember, deletedMember: GroupMember) + case leftMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case groupDeleted(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact) + case groupInvitation(user: UserRef, groupInfo: GroupInfo) // unused + case userJoinedGroup(user: UserRef, groupInfo: GroupInfo) + case joinedGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember) + case connectedToGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?) + case groupRemoved(user: UserRef, groupInfo: GroupInfo) // unused + case groupUpdated(user: UserRef, toGroup: GroupInfo) + case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) + case groupLink(user: UserRef, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) + case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) + case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) + case newMemberContactReceivedInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) // receiving file events - case rcvFileAccepted(user: User, chatItem: AChatItem) - case rcvFileAcceptedSndCancelled(user: User, rcvFileTransfer: RcvFileTransfer) - case rcvFileStart(user: User, chatItem: AChatItem) - case rcvFileProgressXFTP(user: User, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64) - case rcvFileComplete(user: User, chatItem: AChatItem) - case rcvFileCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileSndCancelled(user: User, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileError(user: User, chatItem: AChatItem) + case rcvFileAccepted(user: UserRef, chatItem: AChatItem) + case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer) + case rcvFileStart(user: UserRef, chatItem: AChatItem) + case rcvFileProgressXFTP(user: UserRef, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64) + case rcvFileComplete(user: UserRef, chatItem: AChatItem) + case rcvFileCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) + case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) + case rcvFileError(user: UserRef, chatItem: AChatItem) // sending file events - case sndFileStart(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) - case sndFileComplete(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) - case sndFileCancelled(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) - case sndFileRcvCancelled(user: User, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) - case sndFileProgressXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) - case sndFileCompleteXFTP(user: User, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) - case sndFileError(user: User, chatItem: AChatItem) + case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileCancelled(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer]) + case sndFileRcvCancelled(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) + case sndFileProgressXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64) + case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) + case sndFileError(user: UserRef, chatItem: AChatItem) // call events case callInvitation(callInvitation: RcvCallInvitation) - case callOffer(user: User, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) - case callAnswer(user: User, contact: Contact, answer: WebRTCSession) - case callExtraInfo(user: User, contact: Contact, extraInfo: WebRTCExtraInfo) - case callEnded(user: User, contact: Contact) + case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) + case callAnswer(user: UserRef, contact: Contact, answer: WebRTCSession) + case callExtraInfo(user: UserRef, contact: Contact, extraInfo: WebRTCExtraInfo) + case callEnded(user: UserRef, contact: Contact) case callInvitations(callInvitations: [RcvCallInvitation]) case ntfTokenStatus(status: NtfTknStatus) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode) case ntfMessages(user_: User?, connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) - case newContactConnection(user: User, connection: PendingContactConnection) - case contactConnectionDeleted(user: User, connection: PendingContactConnection) + case newContactConnection(user: UserRef, connection: PendingContactConnection) + case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) - case cmdOk(user: User?) - case chatCmdError(user_: User?, chatError: ChatError) - case chatError(user_: User?, chatError: ChatError) + case cmdOk(user: UserRef?) + case chatCmdError(user_: UserRef?, chatError: ChatError) + case chatError(user_: UserRef?, chatError: ChatError) case archiveImported(archiveErrors: [ArchiveError]) public var responseType: String { @@ -575,9 +593,11 @@ public enum ChatResponse: Decodable, Error { case .groupMemberCode: return "groupMemberCode" case .connectionVerified: return "connectionVerified" case .invitation: return "invitation" + case .connectionIncognitoUpdated: return "connectionIncognitoUpdated" case .sentConfirmation: return "sentConfirmation" case .sentInvitation: return "sentInvitation" case .contactAlreadyExists: return "contactAlreadyExists" + case .contactRequestAlreadyAccepted: return "contactRequestAlreadyAccepted" case .contactDeleted: return "contactDeleted" case .chatCleared: return "chatCleared" case .userProfileNoChange: return "userProfileNoChange" @@ -607,6 +627,7 @@ public enum ChatResponse: Decodable, Error { case .newChatItem: return "newChatItem" case .chatItemStatusUpdated: return "chatItemStatusUpdated" case .chatItemUpdated: return "chatItemUpdated" + case .chatItemNotChanged: return "chatItemNotChanged" case .chatItemReaction: return "chatItemReaction" case .chatItemDeleted: return "chatItemDeleted" case .contactsList: return "contactsList" @@ -635,6 +656,9 @@ public enum ChatResponse: Decodable, Error { case .groupLinkCreated: return "groupLinkCreated" case .groupLink: return "groupLink" case .groupLinkDeleted: return "groupLinkDeleted" + case .newMemberContact: return "newMemberContact" + case .newMemberContactSentInv: return "newMemberContactSentInv" + case .newMemberContactReceivedInv: return "newMemberContactReceivedInv" case .rcvFileAccepted: return "rcvFileAccepted" case .rcvFileAcceptedSndCancelled: return "rcvFileAcceptedSndCancelled" case .rcvFileStart: return "rcvFileStart" @@ -704,14 +728,16 @@ public enum ChatResponse: Decodable, Error { case let .contactCode(u, contact, connectionCode): return withUser(u, "contact: \(String(describing: contact))\nconnectionCode: \(connectionCode)") case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)") case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") - case let .invitation(u, connReqInvitation): return withUser(u, connReqInvitation) + case let .invitation(u, connReqInvitation, _): return withUser(u, connReqInvitation) + case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) case .sentConfirmation: return noDetails case .sentInvitation: return noDetails case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) + case let .contactRequestAlreadyAccepted(u, contact): return withUser(u, String(describing: contact)) case let .contactDeleted(u, contact): return withUser(u, String(describing: contact)) case let .chatCleared(u, chatInfo): return withUser(u, String(describing: chatInfo)) case .userProfileNoChange: return noDetails - case let .userProfileUpdated(u, _, toProfile): return withUser(u, String(describing: toProfile)) + case let .userProfileUpdated(u, _, toProfile, _): return withUser(u, String(describing: toProfile)) case let .userPrivacy(u, updatedUser): return withUser(u, String(describing: updatedUser)) case let .contactAliasUpdated(u, toContact): return withUser(u, String(describing: toContact)) case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) @@ -737,6 +763,7 @@ public enum ChatResponse: Decodable, Error { case let .newChatItem(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemStatusUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) + case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)") case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) @@ -765,6 +792,9 @@ public enum ChatResponse: Decodable, Error { case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)") case let .groupLink(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)") case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) + case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") + case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem)) case .rcvFileAcceptedSndCancelled: return noDetails case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem)) @@ -802,7 +832,7 @@ public enum ChatResponse: Decodable, Error { private var noDetails: String { get { "\(responseType): no details" } } - private func withUser(_ u: User?, _ s: String) -> String { + private func withUser(_ u: (any UserLike)?, _ s: String) -> String { if let id = u?.userId { return "userId: \(id)\n\(s)" } @@ -810,6 +840,14 @@ public enum ChatResponse: Decodable, Error { } } +public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? { + switch chatResponse { + case let .chatCmdError(_, .error(error)): return error + case let .chatError(_, .error(error)): return error + default: return nil + } +} + struct NewUser: Encodable { var profile: Profile? var sameServers: Bool @@ -831,7 +869,7 @@ public enum ChatPagination { } struct ComposedMessage: Encodable { - var filePath: String? + var fileSource: CryptoFile? var quotedItemId: Int64? var msgContent: MsgContent } @@ -1054,7 +1092,7 @@ public struct NetCfg: Codable, Equatable { sessionMode: TransportSessionMode.user, tcpConnectTimeout: 15_000_000, tcpTimeout: 10_000_000, - tcpTimeoutPerKb: 20_000, + tcpTimeoutPerKb: 30_000, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -1066,7 +1104,7 @@ public struct NetCfg: Codable, Equatable { sessionMode: TransportSessionMode.user, tcpConnectTimeout: 30_000_000, tcpTimeout: 20_000_000, - tcpTimeoutPerKb: 40_000, + tcpTimeoutPerKb: 60_000, tcpKeepAlive: KeepAliveOpts.defaults, smpPingInterval: 1200_000_000, smpPingCount: 3, @@ -1362,14 +1400,32 @@ public enum ChatError: Decodable { public enum ChatErrorType: Decodable { case noActiveUser + case noConnectionUser(agentConnId: String) + case noSndFileUser(agentSndFileId: String) + case noRcvFileUser(agentRcvFileId: String) + case userUnknown case activeUserExists case userExists - case differentActiveUser + case differentActiveUser(commandUserId: Int64, activeUserId: Int64) + case cantDeleteActiveUser(userId: Int64) + case cantDeleteLastUser(userId: Int64) + case cantHideLastUser(userId: Int64) + case hiddenUserAlwaysMuted(userId: Int64) + case emptyUserPassword(userId: Int64) + case userAlreadyHidden(userId: Int64) + case userNotHidden(userId: Int64) case chatNotStarted + case chatNotStopped + case chatStoreChanged case invalidConnReq - case invalidChatMessage(message: String) + case invalidChatMessage(connection: Connection, message: String) case contactNotReady(contact: Contact) - case groupUserRole + case contactDisabled(contact: Contact) + case connectionDisabled(connection: Connection) + case groupUserRole(groupInfo: GroupInfo, requiredRole: GroupMemberRole) + case groupMemberInitialRole(groupInfo: GroupInfo, initialRole: GroupMemberRole) + case contactIncognitoCantInvite + case groupIncognitoCantInvite case groupContactRole(contactName: ContactName) case groupDuplicateMember(contactName: ContactName) case groupDuplicateMemberId @@ -1381,25 +1437,54 @@ public enum ChatErrorType: Decodable { case groupCantResendInvitation(groupInfo: GroupInfo, contactName: ContactName) case groupInternal(message: String) case fileNotFound(message: String) + case fileSize(filePath: String) case fileAlreadyReceiving(message: String) + case fileCancelled(message: String) + case fileCancel(fileId: Int64, message: String) case fileAlreadyExists(filePath: String) case fileRead(filePath: String, message: String) case fileWrite(filePath: String, message: String) case fileSend(fileId: Int64, agentError: String) case fileRcvChunk(message: String) case fileInternal(message: String) + case fileImageType(filePath: String) + case fileImageSize(filePath: String) + case fileNotReceived(fileId: Int64) + // case xFTPRcvFile + // case xFTPSndFile + case fallbackToSMPProhibited(fileId: Int64) + case inlineFileProhibited(fileId: Int64) case invalidQuote case invalidChatItemUpdate case invalidChatItemDelete + case hasCurrentCall + case noCurrentCall + case callContact(contactId: Int64) + case callState + case directMessagesProhibited(contact: Contact) case agentVersion + case agentNoSubResult(agentConnId: String) case commandError(message: String) + case serverProtocol + case agentCommandError(message: String) + case invalidFileDescription(message: String) + case connectionIncognitoChangeProhibited + case peerChatVRangeIncompatible + case internalError(message: String) case exception(message: String) } public enum StoreError: Decodable { case duplicateName + case userNotFound(userId: Int64) + case userNotFoundByName(contactName: ContactName) + case userNotFoundByContactId(contactId: Int64) + case userNotFoundByGroupId(groupId: Int64) + case userNotFoundByFileId(fileId: Int64) + case userNotFoundByContactRequestId(contactRequestId: Int64) case contactNotFound(contactId: Int64) case contactNotFoundByName(contactName: ContactName) + case contactNotFoundByMemberId(groupMemberId: Int64) case contactNotReady(contactName: ContactName) case duplicateContactLink case userContactLinkNotFound @@ -1407,6 +1492,10 @@ public enum StoreError: Decodable { case contactRequestNotFoundByName(contactName: ContactName) case groupNotFound(groupId: Int64) case groupNotFoundByName(groupName: GroupName) + case groupMemberNameNotFound(groupId: Int64, groupMemberName: ContactName) + case groupMemberNotFound(groupMemberId: Int64) + case groupMemberNotFoundByMemberId(memberId: String) + case memberContactGroupMemberNotFound(contactId: Int64) case groupWithoutUser case duplicateGroupMember case groupAlreadyJoined @@ -1414,9 +1503,17 @@ public enum StoreError: Decodable { case sndFileNotFound(fileId: Int64) case sndFileInvalid(fileId: Int64) case rcvFileNotFound(fileId: Int64) + case rcvFileDescrNotFound(fileId: Int64) case fileNotFound(fileId: Int64) case rcvFileInvalid(fileId: Int64) + case rcvFileInvalidDescrPart + case sharedMsgIdNotFoundByFileId(fileId: Int64) + case fileIdNotFoundBySharedMsgId(sharedMsgId: String) + case sndFileNotFoundXFTP(agentSndFileId: String) + case rcvFileNotFoundXFTP(agentRcvFileId: String) case connectionNotFound(agentConnId: String) + case connectionNotFoundById(connId: Int64) + case connectionNotFoundByMemberId(groupMemberId: Int64) case pendingConnectionNotFound(connId: Int64) case introNotFound case uniqueID @@ -1424,11 +1521,16 @@ public enum StoreError: Decodable { case noMsgDelivery(connId: Int64, agentMsgId: String) case badChatItem(itemId: Int64) case chatItemNotFound(itemId: Int64) - case quotedChatItemNotFound + case chatItemNotFoundByText(text: String) case chatItemSharedMsgIdNotFound(sharedMsgId: String) case chatItemNotFoundByFileId(fileId: Int64) + case chatItemNotFoundByGroupId(groupId: Int64) + case profileNotFound(profileId: Int64) case duplicateGroupLink(groupInfo: GroupInfo) case groupLinkNotFound(groupInfo: GroupInfo) + case hostMemberIdNotFound(groupId: Int64) + case contactNotFoundByFileId(fileId: Int64) + case noGroupSndStatus(itemId: Int64, groupMemberId: Int64) } public enum DatabaseError: Decodable { @@ -1448,11 +1550,12 @@ public enum AgentErrorType: Decodable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) case SMP(smpErr: ProtocolErrorType) - case XFTP(xftpErr: XFTPErrorType) case NTF(ntfErr: ProtocolErrorType) + case XFTP(xftpErr: XFTPErrorType) case BROKER(brokerAddress: String, brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) case INTERNAL(internalErr: String) + case INACTIVE } public enum CommandErrorType: Decodable { @@ -1472,9 +1575,10 @@ public enum ConnectionErrorType: Decodable { } public enum BrokerErrorType: Decodable { - case RESPONSE(smpErr: ProtocolErrorType) + case RESPONSE(smpErr: String) case UNEXPECTED case NETWORK + case HOST case TRANSPORT(transportErr: ProtocolTransportError) case TIMEOUT } @@ -1508,6 +1612,7 @@ public enum XFTPErrorType: Decodable { public enum ProtocolCommandError: Decodable { case UNKNOWN case SYNTAX + case PROHIBITED case NO_AUTH case HAS_AUTH case NO_ENTITY @@ -1530,7 +1635,9 @@ public enum SMPAgentError: Decodable { case A_MESSAGE case A_PROHIBITED case A_VERSION - case A_ENCRYPTION + case A_CRYPTO + case A_DUPLICATE + case A_QUEUE(queueErr: String) } public enum ArchiveError: Decodable { diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 65907d89f3..e09b957171 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -17,6 +17,7 @@ public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used +public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" @@ -29,7 +30,7 @@ let GROUP_DEFAULT_NETWORK_ENABLE_KEEP_ALIVE = "networkEnableKeepAlive" let GROUP_DEFAULT_NETWORK_TCP_KEEP_IDLE = "networkTCPKeepIdle" let GROUP_DEFAULT_NETWORK_TCP_KEEP_INTVL = "networkTCPKeepIntvl" let GROUP_DEFAULT_NETWORK_TCP_KEEP_CNT = "networkTCPKeepCnt" -let GROUP_DEFAULT_INCOGNITO = "incognito" +public let GROUP_DEFAULT_INCOGNITO = "incognito" let GROUP_DEFAULT_STORE_DB_PASSPHRASE = "storeDBPassphrase" let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphrase" public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" @@ -59,6 +60,7 @@ public func registerGroupDefaults() { GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, + GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, ]) @@ -113,7 +115,7 @@ public let ntfEnablePeriodicGroupDefault = BoolDefault(defaults: groupDefaults, public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) -public let privacyTransferImagesInlineGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE) +public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index cfad3cc691..c0ec048572 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI -public struct User: Decodable, NamedChat, Identifiable { +public struct User: Identifiable, Decodable, UserLike, NamedChat { public var userId: Int64 var userContactId: Int64 var localDisplayName: ContactName @@ -52,6 +52,17 @@ public struct User: Decodable, NamedChat, Identifiable { ) } +public struct UserRef: Identifiable, Decodable, UserLike { + public var userId: Int64 + public var localDisplayName: ContactName + + public var id: Int64 { userId } +} + +public protocol UserLike: Identifiable { + var userId: Int64 { get } +} + public struct UserPwdHash: Decodable { public var hash: String public var salt: String @@ -160,6 +171,13 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences) } +public struct UserProfileUpdateSummary: Decodable { + public var notChanged: Int + public var updateSuccesses: Int + public var updateFailures: Int + public var changedContacts: [Contact] +} + public enum ChatType: String { case direct = "@" case group = "#" @@ -1200,6 +1218,13 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public var groupInfo: GroupInfo? { + switch self { + case let .group(groupInfo): return groupInfo + default: return nil + } + } + // this works for features that are common for contacts and groups public func featureEnabled(_ feature: ChatFeature) -> Bool { switch self { @@ -1353,11 +1378,17 @@ public struct Contact: Identifiable, Decodable, NamedChat { public var mergedPreferences: ContactUserPreferences var createdAt: Date var updatedAt: Date + var contactGroupMemberId: Int64? + var contactGrpInvSent: Bool public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } } public var ready: Bool { get { activeConn.connStatus == .ready } } - public var sendMsgEnabled: Bool { get { !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false) } } + public var sendMsgEnabled: Bool { get { + (ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?? false)) + || nextSendGrpInv + } } + public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } @@ -1403,7 +1434,8 @@ public struct Contact: Identifiable, Decodable, NamedChat { userPreferences: Preferences.sampleData, mergedPreferences: ContactUserPreferences.sampleData, createdAt: .now, - updatedAt: .now + updatedAt: .now, + contactGrpInvSent: false ) } @@ -1424,6 +1456,7 @@ public struct ContactSubStatus: Decodable { public struct Connection: Decodable { public var connId: Int64 public var agentConnId: String + public var peerChatVRange: VersionRange var connStatus: ConnStatus public var connLevel: Int public var viaGroupLink: Bool @@ -1433,7 +1466,7 @@ public struct Connection: Decodable { public var connectionStats: ConnectionStats? = nil private enum CodingKeys: String, CodingKey { - case connId, agentConnId, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode + case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode } public var id: ChatId { get { ":\(connId)" } } @@ -1441,12 +1474,27 @@ public struct Connection: Decodable { static let sampleData = Connection( connId: 1, agentConnId: "abc", + peerChatVRange: VersionRange(minVersion: 1, maxVersion: 1), connStatus: .ready, connLevel: 0, viaGroupLink: false ) } +public struct VersionRange: Decodable { + public init(minVersion: Int, maxVersion: Int) { + self.minVersion = minVersion + self.maxVersion = maxVersion + } + + public var minVersion: Int + public var maxVersion: Int + + public func isCompatibleRange(_ vRange: VersionRange) -> Bool { + self.minVersion <= vRange.maxVersion && vRange.minVersion <= self.maxVersion + } +} + public struct SecurityCode: Decodable, Equatable { public init(securityCode: String, verifiedAt: Date) { self.securityCode = securityCode @@ -1459,6 +1507,8 @@ public struct SecurityCode: Decodable, Equatable { public struct UserContact: Decodable { public var userContactLinkId: Int64 +// public var connReqContact: String + public var groupId: Int64? public init(userContactLinkId: Int64) { self.userContactLinkId = userContactLinkId @@ -1476,6 +1526,7 @@ public struct UserContact: Decodable { public struct UserContactRequest: Decodable, NamedChat { var contactRequestId: Int64 public var userContactLinkId: Int64 + public var cReqChatVRange: VersionRange var localDisplayName: ContactName var profile: Profile var createdAt: Date @@ -1493,6 +1544,7 @@ public struct UserContactRequest: Decodable, NamedChat { public static let sampleData = UserContactRequest( contactRequestId: 1, userContactLinkId: 1, + cReqChatVRange: VersionRange(minVersion: 1, maxVersion: 1), localDisplayName: "alice", profile: Profile.sampleData, createdAt: .now, @@ -1920,6 +1972,16 @@ public enum ConnectionEntity: Decodable { return nil } } + + public var ntfsEnabled: Bool { + switch self { + case let .rcvDirectMsgConnection(contact): return contact?.chatSettings.enableNtfs ?? false + case let .rcvGroupMsgConnection(groupInfo, _): return groupInfo.chatSettings.enableNtfs + case .sndFileConnection: return false + case .rcvFileConnection: return false + case let .userContactConnection(userContact): return userContact.groupId == nil + } + } } public struct NtfMsgInfo: Decodable { @@ -2002,6 +2064,17 @@ public struct ChatItem: Identifiable, Decodable { } } + public var memberConnected: GroupMember? { + switch chatDir { + case .groupRcv(let groupMember): + switch content { + case .rcvGroupEvent(rcvGroupEvent: .memberConnected): return groupMember + default: return nil + } + default: return nil + } + } + private var showNtfDir: Bool { return !chatDir.sent } @@ -2030,6 +2103,7 @@ public struct ChatItem: Identifiable, Decodable { case .memberLeft: return false case .memberDeleted: return false case .invitedViaGroupLink: return false + case .memberCreatedContact: return false } case .sndGroupEvent: return showNtfDir case .rcvConnEvent: return false @@ -2064,6 +2138,16 @@ public struct ChatItem: Identifiable, Decodable { return nil } + public var encryptedFile: Bool? { + guard let fileSource = file?.fileSource else { return nil } + return fileSource.cryptoArgs != nil + } + + public var encryptLocalFile: Bool { + content.msgContent?.isVideo == false && + privacyEncryptLocalFilesGroupDefault.get() + } + public var memberDisplayName: String? { get { if case let .groupRcv(groupMember) = chatDir { @@ -2263,19 +2347,7 @@ public struct CIMeta: Decodable { } public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { - switch itemStatus { - case .sndSent: return ("checkmark", metaColor) - case let .sndRcvd(msgRcptStatus): - switch msgRcptStatus { - case .ok: return ("checkmark", metaColor) // ("checkmark.circle", metaColor) - case .badMsgHash: return ("checkmark", .red) // ("checkmark.circle", .red) - } - case .sndErrorAuth: return ("multiply", .red) - case .sndError: return ("exclamationmark.triangle.fill", .yellow) - case .rcvNew: return ("circlebadge.fill", Color.accentColor) - case .invalid: return ("questionmark", metaColor) - default: return nil - } + itemStatus.statusIcon(metaColor) } public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, editable: Bool = true) -> CIMeta { @@ -2338,8 +2410,8 @@ private func recent(_ date: Date) -> Bool { public enum CIStatus: Decodable { case sndNew - case sndSent - case sndRcvd(msgRcptStatus: MsgReceiptStatus) + case sndSent(sndProgress: SndCIStatusProgress) + case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress) case sndErrorAuth case sndError(agentError: String) case rcvNew @@ -2358,6 +2430,45 @@ public enum CIStatus: Decodable { case .invalid: return "invalid" } } + + public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { + switch self { + case .sndNew: return nil + case .sndSent: return ("checkmark", metaColor) + case let .sndRcvd(msgRcptStatus, _): + switch msgRcptStatus { + case .ok: return ("checkmark", metaColor) + case .badMsgHash: return ("checkmark", .red) + } + case .sndErrorAuth: return ("multiply", .red) + case .sndError: return ("exclamationmark.triangle.fill", .yellow) + case .rcvNew: return ("circlebadge.fill", Color.accentColor) + case .rcvRead: return nil + case .invalid: return ("questionmark", metaColor) + } + } + + public var statusInfo: (String, String)? { + switch self { + case .sndNew: return nil + case .sndSent: return nil + case .sndRcvd: return nil + case .sndErrorAuth: return ( + NSLocalizedString("Message delivery error", comment: "item status text"), + NSLocalizedString("Most likely this connection is deleted.", comment: "item status description") + ) + case let .sndError(agentError): return ( + NSLocalizedString("Message delivery error", comment: "item status text"), + String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError) + ) + case .rcvNew: return nil + case .rcvRead: return nil + case let .invalid(text): return ( + NSLocalizedString("Invalid status", comment: "item status text"), + text + ) + } + } } public enum MsgReceiptStatus: String, Decodable { @@ -2365,6 +2476,11 @@ public enum MsgReceiptStatus: String, Decodable { case badMsgHash } +public enum SndCIStatusProgress: String, Decodable { + case partial + case complete +} + public enum CIDeleted: Decodable { case deleted(deletedTs: Date?) case moderated(deletedTs: Date?, byGroupMember: GroupMember) @@ -2468,6 +2584,20 @@ public enum CIContent: Decodable, ItemContent { } } } + + public var showMemberName: Bool { + switch self { + case .rcvMsgContent: return true + case .rcvDeleted: return true + case .rcvCall: return true + case .rcvIntegrityError: return true + case .rcvDecryptionError: return true + case .rcvGroupInvitation: return true + case .rcvModerated: return true + case .invalidJSON: return true + default: return false + } + } } public enum MsgDecryptError: String, Decodable { @@ -2596,12 +2726,18 @@ public struct CIFile: Decodable { public var fileId: Int64 public var fileName: String public var fileSize: Int64 - public var filePath: String? + public var fileSource: CryptoFile? public var fileStatus: CIFileStatus public var fileProtocol: FileProtocol public static func getSample(fileId: Int64 = 1, fileName: String = "test.txt", fileSize: Int64 = 100, filePath: String? = "test.txt", fileStatus: CIFileStatus = .rcvComplete) -> CIFile { - CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, filePath: filePath, fileStatus: fileStatus, fileProtocol: .xftp) + let f: CryptoFile? + if let filePath = filePath { + f = CryptoFile.plain(filePath) + } else { + f = nil + } + return CIFile(fileId: fileId, fileName: fileName, fileSize: fileSize, fileSource: f, fileStatus: fileStatus, fileProtocol: .xftp) } public var loaded: Bool { @@ -2648,6 +2784,25 @@ public struct CIFile: Decodable { } } +public struct CryptoFile: Codable { + public var filePath: String // the name of the file, not a full path + public var cryptoArgs: CryptoFileArgs? + + public init(filePath: String, cryptoArgs: CryptoFileArgs?) { + self.filePath = filePath + self.cryptoArgs = cryptoArgs + } + + public static func plain(_ f: String) -> CryptoFile { + CryptoFile(filePath: f, cryptoArgs: nil) + } +} + +public struct CryptoFileArgs: Codable { + public var fileKey: String + public var fileNonce: String +} + public struct CancelAction { public var uiAction: String public var alert: AlertInfo @@ -3051,6 +3206,7 @@ public enum RcvGroupEvent: Decodable { case groupDeleted case groupUpdated(groupProfile: GroupProfile) case invitedViaGroupLink + case memberCreatedContact var text: String { switch self { @@ -3068,6 +3224,7 @@ public enum RcvGroupEvent: Decodable { case .groupDeleted: return NSLocalizedString("deleted group", comment: "rcv group event chat item") case .groupUpdated: return NSLocalizedString("updated group profile", comment: "rcv group event chat item") case .invitedViaGroupLink: return NSLocalizedString("invited via your group link", comment: "rcv group event chat item") + case .memberCreatedContact: return NSLocalizedString("connected directly", comment: "rcv group event chat item") } } } @@ -3212,6 +3369,7 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { public struct ChatItemInfo: Decodable { public var itemVersions: [ChatItemVersion] + public var memberDeliveryStatuses: [MemberDeliveryStatus]? } public struct ChatItemVersion: Decodable { @@ -3221,3 +3379,8 @@ public struct ChatItemVersion: Decodable { public var itemVersionTs: Date public var createdAt: Date } + +public struct MemberDeliveryStatus: Decodable { + public var groupMemberId: Int64 + public var memberDeliveryStatus: CIStatus +} diff --git a/apps/ios/SimpleXChat/CryptoFile.swift b/apps/ios/SimpleXChat/CryptoFile.swift new file mode 100644 index 0000000000..dcb2be9ae0 --- /dev/null +++ b/apps/ios/SimpleXChat/CryptoFile.swift @@ -0,0 +1,69 @@ +// +// CryptoFile.swift +// SimpleX (iOS) +// +// Created by Evgeny on 05/09/2023. +// Copyright © 2023 SimpleX Chat. All rights reserved. +// + +import Foundation + +enum WriteFileResult: Decodable { + case result(cryptoArgs: CryptoFileArgs) + case error(writeError: String) +} + +public func writeCryptoFile(path: String, data: Data) throws -> CryptoFileArgs { + let ptr: UnsafeMutableRawPointer = malloc(data.count) + memcpy(ptr, (data as NSData).bytes, data.count) + var cPath = path.cString(using: .utf8)! + let cjson = chat_write_file(&cPath, ptr, Int32(data.count))! + let d = fromCString(cjson).data(using: .utf8)! + switch try jsonDecoder.decode(WriteFileResult.self, from: d) { + case let .result(cfArgs): return cfArgs + case let .error(err): throw RuntimeError(err) + } +} + +public func readCryptoFile(path: String, cryptoArgs: CryptoFileArgs) throws -> Data { + var cPath = path.cString(using: .utf8)! + var cKey = cryptoArgs.fileKey.cString(using: .utf8)! + var cNonce = cryptoArgs.fileNonce.cString(using: .utf8)! + let ptr = chat_read_file(&cPath, &cKey, &cNonce)! + let status = UInt8(ptr.pointee) + switch status { + case 0: // ok + let dLen = Data(bytes: ptr.advanced(by: 1), count: 4) + let len = dLen.withUnsafeBytes { $0.load(as: UInt32.self) } + let d = Data(bytes: ptr.advanced(by: 5), count: Int(len)) + free(ptr) + return d + case 1: // error + let err = String.init(cString: ptr) + free(ptr) + throw RuntimeError(err) + default: + throw RuntimeError("unexpected chat_read_file status: \(status)") + } +} + +public func encryptCryptoFile(fromPath: String, toPath: String) throws -> CryptoFileArgs { + var cFromPath = fromPath.cString(using: .utf8)! + var cToPath = toPath.cString(using: .utf8)! + let cjson = chat_encrypt_file(&cFromPath, &cToPath)! + let d = fromCString(cjson).data(using: .utf8)! + switch try jsonDecoder.decode(WriteFileResult.self, from: d) { + case let .result(cfArgs): return cfArgs + case let .error(err): throw RuntimeError(err) + } +} + +public func decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) throws { + var cFromPath = fromPath.cString(using: .utf8)! + var cKey = cryptoArgs.fileKey.cString(using: .utf8)! + var cNonce = cryptoArgs.fileNonce.cString(using: .utf8)! + var cToPath = toPath.cString(using: .utf8)! + let cErr = chat_decrypt_file(&cFromPath, &cKey, &cNonce, &cToPath)! + let err = fromCString(cErr) + if err != "" { throw RuntimeError(err) } +} diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 148ab12e29..60d281f146 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -173,11 +173,16 @@ public func getAppFilePath(_ fileName: String) -> URL { getAppFilesDirectory().appendingPathComponent(fileName) } -public func saveFile(_ data: Data, _ fileName: String) -> String? { +public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> CryptoFile? { let filePath = getAppFilePath(fileName) do { - try data.write(to: filePath) - return fileName + if encrypted { + let cfArgs = try writeCryptoFile(path: filePath.path, data: data) + return CryptoFile(filePath: fileName, cryptoArgs: cfArgs) + } else { + try data.write(to: filePath) + return CryptoFile.plain(fileName) + } } catch { logger.error("FileUtils.saveFile error: \(error.localizedDescription)") return nil @@ -210,7 +215,7 @@ public func cleanupFile(_ aChatItem: AChatItem) { let cItem = aChatItem.chatItem let mc = cItem.content.msgContent if case .file = mc, - let fileName = cItem.file?.filePath { + let fileName = cItem.file?.fileSource?.filePath { removeFile(fileName) } } @@ -221,3 +226,15 @@ public func getMaxFileSize(_ fileProtocol: FileProtocol) -> Int64 { case .smp: return MAX_FILE_SIZE_SMP } } + +public struct RuntimeError: Error { + let message: String + + public init(_ message: String) { + self.message = message + } + + public var localizedDescription: String { + return message + } +} diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index 2e8c5c7124..d613ff20ae 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -21,7 +21,7 @@ public let appNotificationId = "chat.simplex.app.notification" let contactHidden = NSLocalizedString("Contact hidden:", comment: "notification") -public func createContactRequestNtf(_ user: User, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent { +public func createContactRequestNtf(_ user: any UserLike, _ contactRequest: UserContactRequest) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryContactRequest, @@ -38,7 +38,7 @@ public func createContactRequestNtf(_ user: User, _ contactRequest: UserContactR ) } -public func createContactConnectedNtf(_ user: User, _ contact: Contact) -> UNMutableNotificationContent { +public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) -> UNMutableNotificationContent { let hideContent = ntfPreviewModeGroupDefault.get() == .hidden return createNotification( categoryIdentifier: ntfCategoryContactConnected, @@ -56,7 +56,7 @@ public func createContactConnectedNtf(_ user: User, _ contact: Contact) -> UNMut ) } -public func createMessageReceivedNtf(_ user: User, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { +public func createMessageReceivedNtf(_ user: any UserLike, _ cInfo: ChatInfo, _ cItem: ChatItem) -> UNMutableNotificationContent { let previewMode = ntfPreviewModeGroupDefault.get() var title: String if case let .group(groupInfo) = cInfo, case let .groupRcv(groupMember) = cItem.chatDir { diff --git a/apps/ios/SimpleXChat/SimpleX.h b/apps/ios/SimpleXChat/SimpleX.h index 199c688f26..67c2fa728c 100644 --- a/apps/ios/SimpleXChat/SimpleX.h +++ b/apps/ios/SimpleXChat/SimpleX.h @@ -25,3 +25,18 @@ extern char *chat_parse_server(char *str); extern char *chat_password_hash(char *pwd, char *salt); extern char *chat_encrypt_media(char *key, char *frame, int len); extern char *chat_decrypt_media(char *key, char *frame, int len); + +// chat_write_file returns null-terminated string with JSON of WriteFileResult +extern char *chat_write_file(char *path, char *data, int len); + +// chat_read_file returns a buffer with: +// result status (1 byte), then if +// status == 0 (success): buffer length (uint32, 4 bytes), buffer of specified length. +// status == 1 (error): null-terminated error message string. +extern char *chat_read_file(char *path, char *key, char *nonce); + +// chat_encrypt_file returns null-terminated string with JSON of WriteFileResult +extern char *chat_encrypt_file(char *fromPath, char *toPath); + +// chat_decrypt_file returns null-terminated string with the error message +extern char *chat_decrypt_file(char *fromPath, char *key, char *nonce, char *toPath); diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..c01e3d7e66 --- /dev/null +++ b/apps/ios/bg.lproj/Localizable.strings @@ -0,0 +1,3711 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" (" = " ("; + +/* No comment provided by engineer. */ +" (can be copied)" = " (може да се копира)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_курсив_"; + +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- свържете се с [директория за услуги](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjd LW3%23%2F%3Fv%3D1-2%26dh %3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (БЕТА)!\n- потвърждениe за доставка (до 20 члена).\n- по-бързо и по-стабилно."; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- по-стабилна доставка на съобщения.\n- малко по-добри групи.\n- и още!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- гласови съобщения до 5 минути.\n- персонализирано време за изчезване.\n- история на редактиране."; + +/* No comment provided by engineer. */ +", " = ", "; + +/* No comment provided by engineer. */ +": " = ": "; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 цветно!"; + +/* No comment provided by engineer. */ +"." = "."; + +/* No comment provided by engineer. */ +"(" = "("; + +/* No comment provided by engineer. */ +")" = ")"; + +/* No comment provided by engineer. */ +"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Допринеси](https://github.com/simplex-chat/simplex-chat#contribute)"; + +/* No comment provided by engineer. */ +"[Send us email](mailto:chat@simplex.chat)" = "[Изпратете ни имейл](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Звезда в GitHub](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Добави нов контакт**: за да създадете своя еднократен QR код или линк за вашия контакт."; + +/* No comment provided by engineer. */ +"**Create link / QR code** for your contact to use." = "**Създай линк / QR код**, който вашият контакт да използва."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**e2e криптиран**аудио разговор"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**e2e криптирано** видео разговор"; + +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**По поверително**: проверявайте новите съобщения на всеки 20 минути. Токенът на устройството се споделя със сървъра за чат SimpleX, но не и колко контакти или съобщения имате."; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Най-поверително**: не използвайте сървъра за известия SimpleX Chat, периодично проверявайте съобщенията във фонов режим (зависи от това колко често използвате приложението)."; + +/* No comment provided by engineer. */ +"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Поставете получения линк** или го отворете в браузъра и докоснете **Отваряне в мобилно приложение**."; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Моля, обърнете внимание**: НЯМА да можете да възстановите или промените паролата, ако я загубите."; + +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Препоръчително**: токенът на устройството и известията се изпращат до сървъра за уведомяване на SimpleX Chat, но не и съдържанието, размерът на съобщението или от кого е."; + +/* No comment provided by engineer. */ +"**Scan QR code**: to connect to your contact in person or via video call." = "**Сканирай QR код**: за да се свържете с вашия контакт лично или чрез видеообаждане."; + +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Внимание**: Незабавните push известия изискват парола, запазена в Keychain."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*удебелен*"; + +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## История"; + +/* copied message info */ +"## In reply to" = "## В отговор на"; + +/* No comment provided by engineer. */ +"#secret#" = "#тайно#"; + +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (текущ)"; + +/* copied message info */ +"%@ (current):" = "%@ (текущ):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / %@"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ %@"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ и %@ са свързани"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ в %2$@:"; + +/* notification title */ +"%@ is connected!" = "%@ е свързан!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ не е потвърдено"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ е потвърдено"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ сървъри"; + +/* notification title */ +"%@ wants to connect!" = "%@ иска да се свърже!"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ и %lld други членове са свързани"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d дни"; + +/* time interval */ +"%d hours" = "%d часа"; + +/* time interval */ +"%d min" = "%d мин."; + +/* time interval */ +"%d months" = "%d месеца"; + +/* time interval */ +"%d sec" = "%d сек."; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d пропуснато(и) съобщение(я)"; + +/* time interval */ +"%d weeks" = "%d седмици"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld избран(и) контакт(а)"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld файл(а) с общ размер от %@"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld членове"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld минути"; + +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld нови езици на интерфейса"; + +/* No comment provided by engineer. */ +"%lld second(s)" = "%lld секунда(и)"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld секунди"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldд"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldч"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldм"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldмесц."; + +/* No comment provided by engineer. */ +"%llds" = "%lldс"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldсед."; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "%u съобщения не успяха да се декриптират."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u пропуснати съобщения."; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Здравейте!</p>\n<p><a href=\"%@\">Свържете се с мен чрез SimpleX Chat</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~зачеркнат~"; + +/* No comment provided by engineer. */ +"0s" = "0s"; + +/* time interval */ +"1 day" = "1 ден"; + +/* time interval */ +"1 hour" = "1 час"; + +/* No comment provided by engineer. */ +"1 minute" = "1 минута"; + +/* time interval */ +"1 month" = "1 месец"; + +/* time interval */ +"1 week" = "1 седмица"; + +/* No comment provided by engineer. */ +"1-time link" = "Еднократен линк"; + +/* No comment provided by engineer. */ +"5 minutes" = "5 минути"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 секунди"; + +/* No comment provided by engineer. */ +"A few more things" = "Още няколко неща"; + +/* notification title */ +"A new contact" = "Нов контакт"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Нов автоматично генериран профил ще бъде споделен."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Ще се използва отделна TCP връзка **за всеки чатпрофил, който имате в приложението**."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Ще се използва отделна TCP връзка **за всеки контакт и член на групата**.\n**Моля, обърнете внимание**: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят."; + +/* No comment provided by engineer. */ +"Abort" = "Откажи"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Откажи смяна на адрес"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Откажи смяна на адрес?"; + +/* No comment provided by engineer. */ +"About SimpleX" = "За SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX address" = "Повече за SimpleX адреса"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "За SimpleX Chat"; + +/* No comment provided by engineer. */ +"above, then choose:" = "по-горе, след това избери:"; + +/* No comment provided by engineer. */ +"Accent color" = "Основен цвят"; + +/* accept contact request via notification + accept incoming call via notification */ +"Accept" = "Приеми"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Приемане на заявка за връзка?"; + +/* notification body */ +"Accept contact request from %@?" = "Приемане на заявка за контакт от %@?"; + +/* accept contact request via notification */ +"Accept incognito" = "Приеми инкогнито"; + +/* call status */ +"accepted call" = "обаждането прието"; + +/* 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." = "Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти."; + +/* No comment provided by engineer. */ +"Add preset servers" = "Добави предварително зададени сървъри"; + +/* No comment provided by engineer. */ +"Add profile" = "Добави профил"; + +/* No comment provided by engineer. */ +"Add server…" = "Добави сървър…"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Добави сървъри чрез сканиране на QR кодове."; + +/* No comment provided by engineer. */ +"Add to another device" = "Добави към друго устройство"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Добави съобщение при посрещане"; + +/* No comment provided by engineer. */ +"Address" = "Адрес"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване."; + +/* member role */ +"admin" = "админ"; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Админите могат да създадат линкове за присъединяване към групи."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Разширени мрежови настройки"; + +/* chat item text */ +"agreeing encryption for %@…" = "съгласуване на криптиране за %@…"; + +/* chat item text */ +"agreeing encryption…" = "съгласуване на криптиране…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Всички данни от приложението бяха изтрити."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Всички чатове и съобщения ще бъдат изтрити - това не може да бъде отменено!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "Всички данни се изтриват при въвеждане."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Всички членове на групата ще останат свързани."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Всички съобщения ще бъдат изтрити - това не може да бъде отменено! Съобщенията ще бъдат изтрити САМО за вас."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Всички ваши контакти ще останат свързани."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Всички ваши контакти ще останат свързани. Актуализацията на профила ще бъде изпратена до вашите контакти."; + +/* No comment provided by engineer. */ +"Allow" = "Позволи"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Позволи обаждания само ако вашият контакт ги разрешава."; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Позволи изчезващи съобщения само ако вашият контакт ги разрешава."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you." = "Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава."; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "Позволи реакции на съобщения само ако вашият контакт ги разрешава."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Позволи реакции на съобщения."; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "Позволи изпращането на лични съобщения до членовете."; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "Разреши изпращането на изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages." = "Позволи необратимо изтриване на изпратените съобщения."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "Позволи изпращане на файлове и медия."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "Позволи изпращане на гласови съобщения."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "Позволи гласови съобщения само ако вашият контакт ги разрешава."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Позволи гласови съобщения?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "Позволи на вашите контакти да добавят реакции към съобщения."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "Позволи на вашите контакти да ви се обаждат."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages." = "Позволи на вашите контакти да изтриват необратимо изпратените съобщения."; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Позволи на вашите контакти да изпращат изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Позволи на вашите контакти да изпращат гласови съобщения."; + +/* No comment provided by engineer. */ +"Already connected?" = "Вече сте свързани?"; + +/* pref value */ +"always" = "винаги"; + +/* No comment provided by engineer. */ +"Always use relay" = "Винаги използвай реле"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено."; + +/* No comment provided by engineer. */ +"Answer call" = "Отговор на повикване"; + +/* No comment provided by engineer. */ +"App build: %@" = "Компилация на приложението: %@"; + +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "Приложението криптира нови локални файлове (с изключение на видеоклипове)."; + +/* No comment provided by engineer. */ +"App icon" = "Икона на приложението"; + +/* No comment provided by engineer. */ +"App passcode" = "Код за достъп до приложението"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Кода за достъп до приложение се заменя с код за самоунищожение."; + +/* No comment provided by engineer. */ +"App version" = "Версия на приложението"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Версия на приложението: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Изглед"; + +/* No comment provided by engineer. */ +"Attach" = "Прикачи"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Аудио и видео разговори"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Аудио и видео разговори"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "аудио разговор (не е e2e криптиран)"; + +/* chat feature */ +"Audio/video calls" = "Аудио/видео разговори"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "Аудио/видео разговорите са забранени."; + +/* PIN entry */ +"Authentication cancelled" = "Идентификацията е отменена"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Неуспешна идентификация"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "Изисква се идентификацията, преди да се осъществи обаждането, но може да пропуснете повиквания."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Идентификацията е недостъпна"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Автоматично приемане"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "Автоматично приемане на заявки за контакт"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Автоматично приемане на изображения"; + +/* No comment provided by engineer. */ +"Back" = "Назад"; + +/* integrity error chat item */ +"bad message hash" = "лош хеш на съобщението"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Лош хеш на съобщението"; + +/* integrity error chat item */ +"bad message ID" = "лошо ID на съобщението"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Лошо ID на съобщението"; + +/* No comment provided by engineer. */ +"Better messages" = "По-добри съобщения"; + +/* No comment provided by engineer. */ +"bold" = "удебелен"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "И вие, и вашият контакт можете да добавяте реакции към съобщението."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages." = "И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения."; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "И вие, и вашият контакт можете да осъществявате обаждания."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "И вие, и вашият контакт можете да изпращате изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "И вие, и вашият контакт можете да изпращате гласови съобщения."; + +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Български, финландски, тайландски и украински - благодарение на потребителите и [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Чрез чат профил (по подразбиране) или [чрез връзка](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА)."; + +/* No comment provided by engineer. */ +"Call already ended!" = "Разговорът вече приключи!"; + +/* call status */ +"call error" = "грешка при повикване"; + +/* call status */ +"call in progress" = "в момента тече разговор"; + +/* call status */ +"calling…" = "повикване…"; + +/* No comment provided by engineer. */ +"Calls" = "Обаждания"; + +/* No comment provided by engineer. */ +"Can't delete user profile!" = "Потребителският профил не може да се изтрие!"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Не може да покани контакта!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Не може да поканят контактите!"; + +/* No comment provided by engineer. */ +"Cancel" = "Отказ"; + +/* feature offered item */ +"cancelled %@" = "отменен %@"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Няма достъп до Keychain за запазване на паролата за базата данни"; + +/* No comment provided by engineer. */ +"Cannot receive file" = "Файлът не може да бъде получен"; + +/* No comment provided by engineer. */ +"Change" = "Промени"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Промяна на паролата на базата данни?"; + +/* authentication reason */ +"Change lock mode" = "Промяна на режима на заключване"; + +/* No comment provided by engineer. */ +"Change member role?" = "Промяна на ролята на члена?"; + +/* authentication reason */ +"Change passcode" = "Промени kодa за достъп"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Промени адреса за получаване"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Промени адреса за получаване?"; + +/* No comment provided by engineer. */ +"Change role" = "Промени ролята"; + +/* authentication reason */ +"Change self-destruct mode" = "Промени режима на самоунищожение"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "Промени кода за достъп за самоунищожение"; + +/* chat item text */ +"changed address for you" = "променен е адреса за вас"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "променена роля от %1$@ на %2$@"; + +/* rcv group event chat item */ +"changed your role to %@" = "променена е вашата ролята на %@"; + +/* chat item text */ +"changing address for %@…" = "промяна на адреса за %@…"; + +/* chat item text */ +"changing address…" = "промяна на адреса…"; + +/* No comment provided by engineer. */ +"Chat archive" = "Архив на чата"; + +/* No comment provided by engineer. */ +"Chat console" = "Конзола"; + +/* No comment provided by engineer. */ +"Chat database" = "База данни за чата"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Базата данни на чата е изтрита"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Базата данни на чат е импортирана"; + +/* No comment provided by engineer. */ +"Chat is running" = "Чатът работи"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Чатът е спрян"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Чат настройки"; + +/* No comment provided by engineer. */ +"Chats" = "Чатове"; + +/* No comment provided by engineer. */ +"Check server address and try again." = "Проверете адреса на сървъра и опитайте отново."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Китайски и Испански интерфейс"; + +/* No comment provided by engineer. */ +"Choose file" = "Избери файл"; + +/* No comment provided by engineer. */ +"Choose from library" = "Избери от библиотеката"; + +/* No comment provided by engineer. */ +"Clear" = "Изчисти"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Изчисти разговора"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Изчисти разговора?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Изчисти проверката"; + +/* No comment provided by engineer. */ +"colored" = "цветен"; + +/* No comment provided by engineer. */ +"Colors" = "Цветове"; + +/* server test step */ +"Compare file" = "Сравни файл"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Сравнете кодовете за сигурност с вашите контакти."; + +/* No comment provided by engineer. */ +"complete" = "завършен"; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "Конфигурирай ICE сървъри"; + +/* No comment provided by engineer. */ +"Confirm" = "Потвърди"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Потвърди актуализаациите на базата данни"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Потвърди новата парола…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Потвърди kодa за достъп"; + +/* No comment provided by engineer. */ +"Confirm password" = "Потвърди парола"; + +/* server test step */ +"Connect" = "Свързване"; + +/* No comment provided by engineer. */ +"Connect directly" = "Свързване директно"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Свързване инкогнито"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "свържете се с разработчиците на SimpleX Chat."; + +/* No comment provided by engineer. */ +"Connect via contact link" = "Свързване чрез линк на контакта"; + +/* No comment provided by engineer. */ +"Connect via group link?" = "Свързване чрез групов линк?"; + +/* No comment provided by engineer. */ +"Connect via link" = "Свърване чрез линк"; + +/* No comment provided by engineer. */ +"Connect via link / QR code" = "Свърване чрез линк/QR код"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Свързване чрез еднократен линк за връзка"; + +/* No comment provided by engineer. */ +"connected" = "свързан"; + +/* No comment provided by engineer. */ +"connecting" = "свързване"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "свързване (прието)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "свързване (обявено)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "свързване (представен)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "свързване (покана за представяне)"; + +/* call status */ +"connecting call" = "разговорът се свързва…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Свързване със сървъра…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Свързване със сървър…(грешка: %@)"; + +/* chat list item title */ +"connecting…" = "свързване…"; + +/* No comment provided by engineer. */ +"Connection" = "Връзка"; + +/* No comment provided by engineer. */ +"Connection error" = "Грешка при свързване"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "Грешка при свързване (AUTH)"; + +/* chat list item title (it should not be shown */ +"connection established" = "установена е връзка"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Заявката за връзка е изпратена!"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Времето на изчакване за установяване на връзката изтече"; + +/* connection information */ +"connection:%@" = "връзка:%@"; + +/* No comment provided by engineer. */ +"Contact allows" = "Контактът позволява"; + +/* No comment provided by engineer. */ +"Contact already exists" = "Контактът вече съществува"; + +/* No comment provided by engineer. */ +"Contact and all messages will be deleted - this cannot be undone!" = "Контактът и всички съобщения ще бъдат изтрити - това не може да бъде отменено!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "контактът има e2e криптиране"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "контактът няма e2e криптиране"; + +/* notification */ +"Contact hidden:" = "Контактът е скрит:"; + +/* notification */ +"Contact is connected" = "Контактът е свързан"; + +/* No comment provided by engineer. */ +"Contact is not connected yet!" = "Контактът все още не е свързан!"; + +/* No comment provided by engineer. */ +"Contact name" = "Име на контакт"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Настройки за контакт"; + +/* No comment provided by engineer. */ +"Contacts" = "Контакти"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "Контактите могат да маркират съобщения за изтриване; ще можете да ги разглеждате."; + +/* No comment provided by engineer. */ +"Continue" = "Продължи"; + +/* chat item action */ +"Copy" = "Копирай"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Версия на ядрото: v%@"; + +/* No comment provided by engineer. */ +"Create" = "Създай"; + +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "Създайте адрес, за да позволите на хората да се свързват с вас."; + +/* server test step */ +"Create file" = "Създай файл"; + +/* No comment provided by engineer. */ +"Create group link" = "Създай групов линк"; + +/* No comment provided by engineer. */ +"Create link" = "Създай линк"; + +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Създайте нов профил в [настолното приложение](https://simplex.chat/downloads/). 💻"; + +/* No comment provided by engineer. */ +"Create one-time invitation link" = "Създай линк за еднократна покана"; + +/* server test step */ +"Create queue" = "Създай опашка"; + +/* No comment provided by engineer. */ +"Create secret group" = "Създай тайна група"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "Създай SimpleX адрес"; + +/* No comment provided by engineer. */ +"Create your profile" = "Създай своя профил"; + +/* No comment provided by engineer. */ +"Created on %@" = "Създаден на %@"; + +/* No comment provided by engineer. */ +"creator" = "създател"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Текущ kод за достъп"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Текуща парола…"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "В момента максималният поддържан размер на файла е %@."; + +/* dropdown time picker choice */ +"custom" = "персонализиран"; + +/* No comment provided by engineer. */ +"Custom time" = "Персонализирано време"; + +/* No comment provided by engineer. */ +"Dark" = "Тъмна"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Понижаване на версията на базата данни"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Базата данни е криптирана!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Паролата за криптиране на базата данни ще бъде актуализирана и съхранена в Keychain.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Паролата за криптиране на базата данни ще бъде актуализирана.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Грешка в базата данни"; + +/* No comment provided by engineer. */ +"Database ID" = "ID в базата данни"; + +/* copied message info */ +"Database ID: %d" = "ID в базата данни: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Идентификатори в базата данни и опция за изолация на транспорта."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "Базата данни е криптирана с автоматично генерирана парола, можете да я промените."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "Базата данни е криптирана с автоматично генерирана парола. Моля, променете я преди експортиране."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Парола за базата данни"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Парола за базата данни и експортиране"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Паролата на базата данни е различна от записаната в Keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Изисква се паролата за базата данни, за да се отвори чата."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Актуализация на базата данни"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "версията на базата данни е по-нова от приложението, но няма миграция надолу за: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "Базата данни ще бъде криптирана и паролата ще бъде съхранена в Keychain.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "Базата данни ще бъде криптирана.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "Базата данни ще бъде мигрирана, когато приложението се рестартира"; + +/* time unit */ +"days" = "дни"; + +/* No comment provided by engineer. */ +"Decentralized" = "Децентрализиран"; + +/* message decrypt error item */ +"Decryption error" = "Грешка при декриптиране"; + +/* pref value */ +"default (%@)" = "по подразбиране (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "по подразбиране (не)"; + +/* No comment provided by engineer. */ +"default (yes)" = "по подразбиране (да)"; + +/* chat item action */ +"Delete" = "Изтрий"; + +/* No comment provided by engineer. */ +"Delete address" = "Изтрий адрес"; + +/* No comment provided by engineer. */ +"Delete address?" = "Изтрий адрес?"; + +/* No comment provided by engineer. */ +"Delete after" = "Изтрий след"; + +/* No comment provided by engineer. */ +"Delete all files" = "Изтрий всички файлове"; + +/* No comment provided by engineer. */ +"Delete archive" = "Изтрий архив"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "Изтриване на архива на чата?"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Изтрий чат профила"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Изтриване на чат профила?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Изтрий връзката"; + +/* No comment provided by engineer. */ +"Delete contact" = "Изтрий контакт"; + +/* No comment provided by engineer. */ +"Delete Contact" = "Изтрий контакт"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Изтрий контакт?"; + +/* No comment provided by engineer. */ +"Delete database" = "Изтрий базата данни"; + +/* server test step */ +"Delete file" = "Изтрий файл"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Изтрий файлове и медия?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Изтрий файловете за всички чат профили"; + +/* chat feature */ +"Delete for everyone" = "Изтрий за всички"; + +/* No comment provided by engineer. */ +"Delete for me" = "Изтрий за мен"; + +/* No comment provided by engineer. */ +"Delete group" = "Изтрий група"; + +/* No comment provided by engineer. */ +"Delete group?" = "Изтрий група?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Изтрий поканата"; + +/* No comment provided by engineer. */ +"Delete link" = "Изтрий линк"; + +/* No comment provided by engineer. */ +"Delete link?" = "Изтрий линк?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Изтрий съобщението на члена?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Изтрий съобщението?"; + +/* No comment provided by engineer. */ +"Delete messages" = "Изтрий съобщенията"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Изтрий съобщенията след"; + +/* No comment provided by engineer. */ +"Delete old database" = "Изтрий старата база данни"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Изтрий старата база данни?"; + +/* No comment provided by engineer. */ +"Delete pending connection" = "Изтрий предстоящата връзка"; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Изтрий предстоящата връзка?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Изтрий профил"; + +/* server test step */ +"Delete queue" = "Изтрий опашка"; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Изтрий потребителския профил?"; + +/* deleted chat item */ +"deleted" = "изтрит"; + +/* No comment provided by engineer. */ +"Deleted at" = "Изтрито на"; + +/* copied message info */ +"Deleted at: %@" = "Изтрито на: %@"; + +/* rcv group event chat item */ +"deleted group" = "групата изтрита"; + +/* No comment provided by engineer. */ +"Delivery" = "Доставка"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Потвърждениeто за доставка е деактивирано!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Потвърждениe за доставка!"; + +/* No comment provided by engineer. */ +"Description" = "Описание"; + +/* No comment provided by engineer. */ +"Develop" = "Разработване"; + +/* No comment provided by engineer. */ +"Developer tools" = "Инструменти за разработчици"; + +/* No comment provided by engineer. */ +"Device" = "Устройство"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Идентификацията на устройството е деактивирано. Изключване на SimpleX заключване."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Идентификацията на устройството не е активирана. Можете да включите SimpleX заключване през Настройки, след като активирате идентификацията на устройството."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "различна миграция в приложението/базата данни: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Различни имена, аватари и транспортна изолация."; + +/* connection level description */ +"direct" = "директна"; + +/* chat feature */ +"Direct messages" = "Лични съобщения"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this group." = "Личните съобщения между членовете са забранени в тази група."; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "Деактивиране (запазване на промените)"; + +/* No comment provided by engineer. */ +"Disable for all" = "Деактивиране за всички"; + +/* authentication reason */ +"Disable SimpleX Lock" = "Деактивирай SimpleX заключване"; + +/* No comment provided by engineer. */ +"disabled" = "деактивирано"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Изчезващо съобщение"; + +/* chat feature */ +"Disappearing messages" = "Изчезващи съобщения"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Изчезващите съобщения са забранени в този чат."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this group." = "Изчезващите съобщения са забранени в тази група."; + +/* No comment provided by engineer. */ +"Disappears at" = "Изчезва в"; + +/* copied message info */ +"Disappears at: %@" = "Изчезва в: %@"; + +/* server test step */ +"Disconnect" = "Прекъсни връзката"; + +/* No comment provided by engineer. */ +"Discover and join groups" = "Открийте и се присъединете към групи"; + +/* No comment provided by engineer. */ +"Display name" = "Показвано Име"; + +/* No comment provided by engineer. */ +"Display name:" = "Показвано име:"; + +/* No comment provided by engineer. */ +"Do it later" = "Отложи"; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "НЕ използвайте SimpleX за спешни повиквания."; + +/* No comment provided by engineer. */ +"Don't create address" = "Не създавай адрес"; + +/* No comment provided by engineer. */ +"Don't enable" = "Не активирай"; + +/* No comment provided by engineer. */ +"Don't show again" = "Не показвай отново"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Понижи версията и отвори чата"; + +/* server test step */ +"Download file" = "Свали файл"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Дублирано показвано име!"; + +/* integrity error chat item */ +"duplicate message" = "дублирано съобщение"; + +/* No comment provided by engineer. */ +"Duration" = "Продължителност"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "e2e криптиран"; + +/* chat item action */ +"Edit" = "Редактирай"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Редактирай групов профил"; + +/* No comment provided by engineer. */ +"Enable" = "Активирай"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Активиране (запазване на промените)"; + +/* No comment provided by engineer. */ +"Enable automatic message deletion?" = "Активиране на автоматично изтриване на съобщения?"; + +/* No comment provided by engineer. */ +"Enable for all" = "Активиране за всички"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Активирай незабавни известия?"; + +/* No comment provided by engineer. */ +"Enable lock" = "Активирай заключване"; + +/* No comment provided by engineer. */ +"Enable notifications" = "Активирай известията"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "Активирай периодични известия?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Активирай самоунищожение"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Активирай kод за достъп за самоунищожение"; + +/* authentication reason */ +"Enable SimpleX Lock" = "Активирай SimpleX заключване"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "Активирай TCP keep-alive"; + +/* enabled status */ +"enabled" = "активирано"; + +/* enabled status */ +"enabled for contact" = "активирано за контакт"; + +/* enabled status */ +"enabled for you" = "активирано за вас"; + +/* No comment provided by engineer. */ +"Encrypt" = "Криптирай"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Криптиране на база данни?"; + +/* No comment provided by engineer. */ +"Encrypt local files" = "Криптирай локални файлове"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Криптиране на съхранените файлове и медия"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Криптирана база данни"; + +/* notification */ +"Encrypted message or another event" = "Криптирано съобщение или друго събитие"; + +/* notification */ +"Encrypted message: database error" = "Криптирано съобщение: грешка в базата данни"; + +/* notification */ +"Encrypted message: database migration error" = "Криптирано съобщение: грешка при мигрирането на база данни"; + +/* notification */ +"Encrypted message: keychain error" = "Криптирано съобщение: грешка в keychain"; + +/* notification */ +"Encrypted message: no passphrase" = "Криптирано съобщение: няма парола"; + +/* notification */ +"Encrypted message: unexpected error" = "Криптирано съобщение: неочаквана грешка"; + +/* chat item text */ +"encryption agreed" = "криптирането е съгласувано"; + +/* chat item text */ +"encryption agreed for %@" = "криптирането е съгласувано за %@"; + +/* chat item text */ +"encryption ok" = "криптирането работи"; + +/* chat item text */ +"encryption ok for %@" = "криптирането работи за %@"; + +/* chat item text */ +"encryption re-negotiation allowed" = "разрешено повторно договаряне на криптиране"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "разрешено повторно договаряне на криптиране за %@"; + +/* chat item text */ +"encryption re-negotiation required" = "необходимо е повторно договаряне на криптиране"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "необходимо е повторно договаряне на криптиране за %@"; + +/* No comment provided by engineer. */ +"ended" = "приключен"; + +/* call status */ +"ended call %@" = "приключи разговор %@"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Въведи правилна парола."; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Въведете kодa за достъп"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Въведи парола…"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "Въведете парола по-горе, за да се покаже!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "Въведи сървъра ръчно"; + +/* placeholder */ +"Enter welcome message…" = "Въведи съобщение при посрещане…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Въведи съобщение при посрещане…(незадължително)"; + +/* No comment provided by engineer. */ +"error" = "грешка"; + +/* No comment provided by engineer. */ +"Error" = "Грешка при свързване със сървъра"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Грешка при отказване на промяна на адреса"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Грешка при приемане на заявка за контакт"; + +/* No comment provided by engineer. */ +"Error accessing database file" = "Грешка при достъпа до файла с базата данни"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Грешка при добавяне на член(ове)"; + +/* No comment provided by engineer. */ +"Error changing address" = "Грешка при промяна на адреса"; + +/* No comment provided by engineer. */ +"Error changing role" = "Грешка при промяна на ролята"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Грешка при промяна на настройката"; + +/* No comment provided by engineer. */ +"Error creating address" = "Грешка при създаване на адрес"; + +/* No comment provided by engineer. */ +"Error creating group" = "Грешка при създаване на група"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Грешка при създаване на групов линк"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Грешка при създаване на профил!"; + +/* No comment provided by engineer. */ +"Error decrypting file" = "Грешка при декриптирането на файла"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Грешка при изтриване на чат базата данни"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Грешка при изтриването на чата!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Грешка при изтриване на връзката"; + +/* No comment provided by engineer. */ +"Error deleting contact" = "Грешка при изтриване на контакт"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Грешка при изтриване на базата данни"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Грешка при изтриване на старата база данни"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Грешка при изтриването на токена"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Грешка при изтриване на потребителския профил"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Грешка при активирането на потвърждениeто за доставка!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Грешка при активирането на известията"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Грешка при криптиране на базата данни"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Грешка при експортиране на чат базата данни"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Грешка при импортиране на чат базата данни"; + +/* No comment provided by engineer. */ +"Error joining group" = "Грешка при присъединяване към група"; + +/* No comment provided by engineer. */ +"Error loading %@ servers" = "Грешка при зареждане на %@ сървъри"; + +/* No comment provided by engineer. */ +"Error receiving file" = "Грешка при получаване на файл"; + +/* No comment provided by engineer. */ +"Error removing member" = "Грешка при отстраняване на член"; + +/* No comment provided by engineer. */ +"Error saving %@ servers" = "Грешка при запазване на %@ сървъра"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Грешка при запазване на профила на групата"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "Грешка при запазване на ICE сървърите"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Грешка при запазване на кода за достъп"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Грешка при запазване на парола в Кeychain"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Грешка при запазване на потребителска парола"; + +/* No comment provided by engineer. */ +"Error sending email" = "Грешка при изпращане на имейл"; + +/* No comment provided by engineer. */ +"Error sending message" = "Грешка при изпращане на съобщение"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Грешка при настройването на потвърждениeто за доставка!!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Грешка при стартиране на чата"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Грешка при спиране на чата"; + +/* No comment provided by engineer. */ +"Error switching profile!" = "Грешка при смяна на профил!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Грешка при синхронизиране на връзката"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Грешка при актуализиране на груповия линк"; + +/* No comment provided by engineer. */ +"Error updating message" = "Грешка при актуализиране на съобщението"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Грешка при актуализиране на настройките"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Грешка при актуализиране на поверителността на потребителя"; + +/* No comment provided by engineer. */ +"Error: " = "Грешка: "; + +/* No comment provided by engineer. */ +"Error: %@" = "Грешка: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Грешка: няма файл с база данни"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Грешка: URL адресът е невалиден"; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Дори когато е деактивиран в разговора."; + +/* No comment provided by engineer. */ +"event happened" = "събитие се случи"; + +/* No comment provided by engineer. */ +"Exit without saving" = "Изход без запазване"; + +/* No comment provided by engineer. */ +"Export database" = "Експортирай база данни"; + +/* No comment provided by engineer. */ +"Export error:" = "Грешка при експортиране:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Експортиран архив на базата данни."; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Експортиране на архив на базата данни…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Премахването на паролата е неуспешно"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Бързо и без чакане, докато подателят е онлайн!"; + +/* No comment provided by engineer. */ +"Favorite" = "Любим"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "Файлът ще бъде изтрит от сървърите."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "Файлът ще бъде получен, когато вашият контакт завърши качването му."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "Файлът ще бъде получен, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!"; + +/* No comment provided by engineer. */ +"File: %@" = "Файл: %@"; + +/* No comment provided by engineer. */ +"Files & media" = "Файлове и медия"; + +/* chat feature */ +"Files and media" = "Файлове и медия"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "Файловете и медията са забранени в тази група."; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Файловете и медията са забранени!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Филтрирайте непрочетените и любимите чатове."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Най-накрая ги имаме! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Намирайте чатове по-бързо"; + +/* No comment provided by engineer. */ +"Fix" = "Поправи"; + +/* No comment provided by engineer. */ +"Fix connection" = "Поправи връзката"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Поправи връзката?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Оправяне на криптирането след възстановяване от резервни копия."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Поправката не се поддържа от контакта"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Поправката не се поддържа от члена на групата"; + +/* No comment provided by engineer. */ +"For console" = "За конзолата"; + +/* No comment provided by engineer. */ +"French interface" = "Френски интерфейс"; + +/* No comment provided by engineer. */ +"Full link" = "Цял линк"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Пълно име (незадължително)"; + +/* No comment provided by engineer. */ +"Full name:" = "Пълно име:"; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Напълно преработено - работи във фонов режим!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Допълнително намален разход на батерията"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIF файлове и стикери"; + +/* No comment provided by engineer. */ +"Group" = "Група"; + +/* No comment provided by engineer. */ +"group deleted" = "групата е изтрита"; + +/* No comment provided by engineer. */ +"Group display name" = "Показвано име на групата"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "Пълно име на групата (незадължително)"; + +/* No comment provided by engineer. */ +"Group image" = "Групово изображение"; + +/* No comment provided by engineer. */ +"Group invitation" = "Групова покана"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "Груповата покана е изтекла"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "Груповата покана вече е невалидна, премахната е от подателя."; + +/* No comment provided by engineer. */ +"Group link" = "Групов линк"; + +/* No comment provided by engineer. */ +"Group links" = "Групови линкове"; + +/* No comment provided by engineer. */ +"Group members can add message reactions." = "Членовете на групата могат да добавят реакции към съобщенията."; + +/* No comment provided by engineer. */ +"Group members can irreversibly delete sent messages." = "Членовете на групата могат необратимо да изтриват изпратените съобщения."; + +/* No comment provided by engineer. */ +"Group members can send direct messages." = "Членовете на групата могат да изпращат лични съобщения."; + +/* No comment provided by engineer. */ +"Group members can send disappearing messages." = "Членовете на групата могат да изпращат изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Group members can send files and media." = "Членовете на групата могат да изпращат файлове и медия."; + +/* No comment provided by engineer. */ +"Group members can send voice messages." = "Членовете на групата могат да изпращат гласови съобщения."; + +/* notification */ +"Group message:" = "Групово съобщение:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Групово модериране"; + +/* No comment provided by engineer. */ +"Group preferences" = "Групови настройки"; + +/* No comment provided by engineer. */ +"Group profile" = "Групов профил"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Груповият профил се съхранява на устройствата на членовете, а не на сървърите."; + +/* snd group event chat item */ +"group profile updated" = "профилът на групата е актуализиран"; + +/* No comment provided by engineer. */ +"Group welcome message" = "Съобщение при посрещане в групата"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Групата ще бъде изтрита за всички членове - това не може да бъде отменено!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "Групата ще бъде изтрита за вас - това не може да бъде отменено!"; + +/* No comment provided by engineer. */ +"Help" = "Помощ"; + +/* No comment provided by engineer. */ +"Hidden" = "Скрит"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Скрити чат профили"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Парола за скрит профил"; + +/* chat item action */ +"Hide" = "Скрий"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Скриване на екрана на приложението в изгледа на скоро отворнените приложения."; + +/* No comment provided by engineer. */ +"Hide profile" = "Скрий профила"; + +/* No comment provided by engineer. */ +"Hide:" = "Скрий:"; + +/* No comment provided by engineer. */ +"History" = "История"; + +/* time unit */ +"hours" = "часове"; + +/* No comment provided by engineer. */ +"How it works" = "Как работи"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "Как работи SimpleX"; + +/* No comment provided by engineer. */ +"How to" = "Информация"; + +/* No comment provided by engineer. */ +"How to use it" = "Как се използва"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Как да използвате вашите сървъри"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "ICE сървъри (по един на ред)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка."; + +/* No comment provided by engineer. */ +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Ако не можете да се срещнете на живо, можете да **сканирате QR код във видеообаждането** или вашият контакт може да сподели линк за покана."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Ако въведете този kод за достъп, когато отваряте приложението, всички данни от приложението ще бъдат необратимо изтрити!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "Ако въведете kодa за достъп за самоунищожение, докато отваряте приложението:"; + +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ако трябва да използвате чата сега, докоснете **Отложи** отдолу (ще ви бъде предложено да мигрирате базата данни, когато рестартирате приложението)."; + +/* No comment provided by engineer. */ +"Ignore" = "Игнорирай"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "Изображението ще бъде получено, когато вашият контакт завърши качването му."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!"; + +/* No comment provided by engineer. */ +"Immediately" = "Веднага"; + +/* No comment provided by engineer. */ +"Immune to spam and abuse" = "Защитен от спам и злоупотреби"; + +/* No comment provided by engineer. */ +"Import" = "Импортиране"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Импортиране на чат база данни?"; + +/* No comment provided by engineer. */ +"Import database" = "Импортиране на база данни"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Подобрена поверителност и сигурност"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Подобрена конфигурация на сървъра"; + +/* No comment provided by engineer. */ +"In reply to" = "В отговор на"; + +/* No comment provided by engineer. */ +"Incognito" = "Инкогнито"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Режим инкогнито"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Режимът инкогнито защитава вашата поверителност, като използва нов автоматично генериран профил за всеки контакт."; + +/* chat list item description */ +"incognito via contact address link" = "инкогнито чрез линк с адрес за контакт"; + +/* chat list item description */ +"incognito via group link" = "инкогнито чрез групов линк"; + +/* chat list item description */ +"incognito via one-time link" = "инкогнито чрез еднократен линк за връзка"; + +/* notification */ +"Incoming audio call" = "Входящо аудио повикване"; + +/* notification */ +"Incoming call" = "Входящо повикване"; + +/* notification */ +"Incoming video call" = "Входящо видео повикване"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несъвместима версия на базата данни"; + +/* PIN entry */ +"Incorrect passcode" = "Неправилен kод за достъп"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Неправилен код за сигурност!"; + +/* connection level description */ +"indirect (%d)" = "индиректна (%d)"; + +/* chat item action */ +"Info" = "Информация"; + +/* No comment provided by engineer. */ +"Initial role" = "Първоначална роля"; + +/* No comment provided by engineer. */ +"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Инсталирайте [SimpleX Chat за терминал](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Незабавните push известия ще бъдат скрити!\n"; + +/* No comment provided by engineer. */ +"Instantly" = "Мигновено"; + +/* No comment provided by engineer. */ +"Interface" = "Интерфейс"; + +/* invalid chat data */ +"invalid chat" = "невалиден чат"; + +/* No comment provided by engineer. */ +"invalid chat data" = "невалидни данни за чат"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Невалиден линк за връзка"; + +/* invalid chat item */ +"invalid data" = "невалидни данни"; + +/* No comment provided by engineer. */ +"Invalid server address!" = "Невалиден адрес на сървъра!"; + +/* item status text */ +"Invalid status" = "Невалиден статус"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "Поканата е изтекла!"; + +/* group name */ +"invitation to group %@" = "покана за група %@"; + +/* No comment provided by engineer. */ +"Invite friends" = "Покани приятели"; + +/* No comment provided by engineer. */ +"Invite members" = "Покани членове"; + +/* No comment provided by engineer. */ +"Invite to group" = "Покани в групата"; + +/* No comment provided by engineer. */ +"invited" = "поканен"; + +/* rcv group event chat item */ +"invited %@" = "поканен %@"; + +/* chat list item title */ +"invited to connect" = "поканен да се свърже"; + +/* rcv group event chat item */ +"invited via your group link" = "поканен чрез вашия групов линк"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain се използва за сигурно съхраняване на парола - позволява получаване на push известия."; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "iOS Keychain ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на push известия."; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Необратимо изтриване на съобщение"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "Необратимото изтриване на съобщения е забранено в този чат."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this group." = "Необратимото изтриване на съобщения е забранено в тази група."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил ."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Това може да се случи, когато вие или вашата връзка използвате старо резервно копие на базата данни."; + +/* No comment provided by engineer. */ +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Това може да се случи, когато:\n1. Времето за пазене на съобщенията е изтекло - в изпращащия клиент е 2 дена а на сървъра е 30.\n2. Декриптирането на съобщението е неуспешно, защото вие или вашият контакт сте използвали старо копие на базата данни.\n3. Връзката е била компрометирана."; + +/* No comment provided by engineer. */ +"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Изглежда, че вече сте свързани чрез този линк. Ако не е така, има грешка (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "Италиански интерфейс"; + +/* No comment provided by engineer. */ +"italic" = "курсив"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Японски интерфейс"; + +/* No comment provided by engineer. */ +"Join" = "Присъединяване"; + +/* No comment provided by engineer. */ +"join as %@" = "присъединяване като %@"; + +/* No comment provided by engineer. */ +"Join group" = "Влез в групата"; + +/* No comment provided by engineer. */ +"Join incognito" = "Влез инкогнито"; + +/* No comment provided by engineer. */ +"Joining group" = "Присъединяване към групата"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Запазете връзките си"; + +/* No comment provided by engineer. */ +"Keychain error" = "Keychain грешка"; + +/* No comment provided by engineer. */ +"KeyChain error" = "KeyChain грешка"; + +/* No comment provided by engineer. */ +"Large file!" = "Голям файл!"; + +/* No comment provided by engineer. */ +"Learn more" = "Научете повече"; + +/* No comment provided by engineer. */ +"Leave" = "Напусни"; + +/* No comment provided by engineer. */ +"Leave group" = "Напусни групата"; + +/* No comment provided by engineer. */ +"Leave group?" = "Напусни групата?"; + +/* rcv group event chat item */ +"left" = "напусна"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Нека да поговорим в SimpleX Chat"; + +/* No comment provided by engineer. */ +"Light" = "Светла"; + +/* No comment provided by engineer. */ +"Limitations" = "Ограничения"; + +/* No comment provided by engineer. */ +"LIVE" = "НА ЖИВО"; + +/* No comment provided by engineer. */ +"Live message!" = "Съобщение на живо!"; + +/* No comment provided by engineer. */ +"Live messages" = "Съобщения на живо"; + +/* No comment provided by engineer. */ +"Local name" = "Локално име"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Само данни за локален профил"; + +/* No comment provided by engineer. */ +"Lock after" = "Заключване след"; + +/* No comment provided by engineer. */ +"Lock mode" = "Режим на заключване"; + +/* No comment provided by engineer. */ +"Make a private connection" = "Добави поверителна връзка"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Накарайте едно съобщение да изчезне"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Направи профила поверителен!"; + +/* No comment provided by engineer. */ +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Уверете се, че %@ сървърните адреси са в правилен формат, разделени на редове и не се дублират (%@)."; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани."; + +/* No comment provided by engineer. */ +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Много хора попитаха: *ако SimpleX няма потребителски идентификатори, как може да доставя съобщения?*"; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Маркирай като изтрито за всички"; + +/* No comment provided by engineer. */ +"Mark read" = "Маркирай като прочетено"; + +/* No comment provided by engineer. */ +"Mark verified" = "Маркирай като проверено"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Форматиране на съобщения"; + +/* marked deleted chat item preview text */ +"marked deleted" = "маркирано като изтрито"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Макс. 30 секунди, получено незабавно."; + +/* member role */ +"member" = "член"; + +/* No comment provided by engineer. */ +"Member" = "Член"; + +/* rcv group event chat item */ +"member connected" = "свързан"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Ролята на члена ще бъде променена на \"%@\". Всички членове на групата ще бъдат уведомени."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Ролята на члена ще бъде променена на \"%@\". Членът ще получи нова покана."; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Членът ще бъде премахнат от групата - това не може да бъде отменено!"; + +/* item status text */ +"Message delivery error" = "Грешка при доставката на съобщението"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Потвърждениe за доставка на съобщения!"; + +/* No comment provided by engineer. */ +"Message draft" = "Чернова на съобщение"; + +/* chat feature */ +"Message reactions" = "Реакции на съобщения"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "Реакциите на съобщения са забранени в този чат."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "Реакциите на съобщения са забранени в тази група."; + +/* notification */ +"message received" = "получено съобщение"; + +/* No comment provided by engineer. */ +"Message text" = "Текст на съобщението"; + +/* No comment provided by engineer. */ +"Messages" = "Съобщения"; + +/* No comment provided by engineer. */ +"Messages & files" = "Съобщения и файлове"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Архивът на базата данни се мигрира…"; + +/* No comment provided by engineer. */ +"Migration error:" = "Грешка при мигриране:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Мигрирането е неуспешно. Докоснете **Пропускане** по-долу, за да продължите да използвате текущата база данни. Моля, докладвайте проблема на разработчиците на приложението чрез чат или имейл [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Миграцията е завършена"; + +/* No comment provided by engineer. */ +"Migrations: %@" = "Миграции: %@"; + +/* time unit */ +"minutes" = "минути"; + +/* call status */ +"missed call" = "пропуснато повикване"; + +/* chat item action */ +"Moderate" = "Модерирай"; + +/* moderated chat item */ +"moderated" = "модерирано"; + +/* No comment provided by engineer. */ +"Moderated at" = "Модерирано в"; + +/* copied message info */ +"Moderated at: %@" = "Модерирано в: %@"; + +/* No comment provided by engineer. */ +"moderated by %@" = "модерирано от %@"; + +/* time unit */ +"months" = "месеци"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Очаквайте скоро още подобрения!"; + +/* item status description */ +"Most likely this connection is deleted." = "Най-вероятно тази връзка е изтрита."; + +/* No comment provided by engineer. */ +"Most likely this contact has deleted the connection with you." = "Най-вероятно този контакт е изтрил връзката с вас."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Множество профили за чат"; + +/* No comment provided by engineer. */ +"Mute" = "Без звук"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Без звук при неактивност!"; + +/* No comment provided by engineer. */ +"Name" = "Име"; + +/* No comment provided by engineer. */ +"Network & servers" = "Мрежа и сървъри"; + +/* No comment provided by engineer. */ +"Network settings" = "Мрежови настройки"; + +/* No comment provided by engineer. */ +"Network status" = "Състояние на мрежата"; + +/* No comment provided by engineer. */ +"never" = "никога"; + +/* notification */ +"New contact request" = "Нова заявка за контакт"; + +/* notification */ +"New contact:" = "Нов контакт:"; + +/* No comment provided by engineer. */ +"New database archive" = "Нов архив на база данни"; + +/* No comment provided by engineer. */ +"New desktop app!" = "Ново настолно приложение!"; + +/* No comment provided by engineer. */ +"New display name" = "Ново показвано име"; + +/* No comment provided by engineer. */ +"New in %@" = "Ново в %@"; + +/* No comment provided by engineer. */ +"New member role" = "Нова членска роля"; + +/* notification */ +"new message" = "ново съобщение"; + +/* notification */ +"New message" = "Ново съобщение"; + +/* No comment provided by engineer. */ +"New Passcode" = "Нов kод за достъп"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Нова парола…"; + +/* pref value */ +"no" = "не"; + +/* No comment provided by engineer. */ +"No" = "Не"; + +/* Authentication unavailable */ +"No app password" = "Приложението няма kод за достъп"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Няма избрани контакти"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Няма контакти за добавяне"; + +/* No comment provided by engineer. */ +"No delivery information" = "Няма информация за доставката"; + +/* No comment provided by engineer. */ +"No device token!" = "Няма токен за устройство!"; + +/* No comment provided by engineer. */ +"no e2e encryption" = "липсва e2e криптиране"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Няма филтрирани чатове"; + +/* No comment provided by engineer. */ +"No group!" = "Групата не е намерена!"; + +/* No comment provided by engineer. */ +"No history" = "Няма история"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Няма разрешение за запис на гласово съобщение"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Няма получени или изпратени файлове"; + +/* copied message info in history */ +"no text" = "няма текст"; + +/* No comment provided by engineer. */ +"Notifications" = "Известия"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Известията са деактивирани!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Сега администраторите могат:\n- да изтриват съобщения на членове.\n- да деактивират членове (роля \"наблюдател\")"; + +/* member role */ +"observer" = "наблюдател"; + +/* enabled status + group pref value */ +"off" = "изключено"; + +/* No comment provided by engineer. */ +"Off" = "Изключено"; + +/* No comment provided by engineer. */ +"Off (Local)" = "Изключено (Локално)"; + +/* feature offered item */ +"offered %@" = "предлага %@"; + +/* feature offered item */ +"offered %@: %@" = "предлага %1$@: %2$@"; + +/* No comment provided by engineer. */ +"Ok" = "Ок"; + +/* No comment provided by engineer. */ +"Old database" = "Стара база данни"; + +/* No comment provided by engineer. */ +"Old database archive" = "Стар архив на база данни"; + +/* group pref value */ +"on" = "включено"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "Линк за еднократна покана"; + +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "За свързване ще са необходими Onion хостове. Изисква се активиране на VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "Ще се използват Onion хостове, когато са налични. Изисква се активиране на VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Няма се използват Onion хостове."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**."; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Само собствениците на групата могат да променят груповите настройки."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Само собствениците на групата могат да активират файлове и медията."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Само собствениците на групата могат да активират гласови съобщения."; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Само вие можете да добавяте реакции на съобщенията."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване)."; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Само вие можете да извършвате разговори."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Само вие можете да изпращате изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Само вие можете да изпращате гласови съобщения."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Само вашият контакт може да добавя реакции на съобщенията."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване)."; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Само вашият контакт може да извършва разговори."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Само вашият контакт може да изпраща изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Само вашият контакт може да изпраща гласови съобщения."; + +/* No comment provided by engineer. */ +"Open chat" = "Отвори чат"; + +/* authentication reason */ +"Open chat console" = "Отвори конзолата"; + +/* No comment provided by engineer. */ +"Open Settings" = "Отвори настройки"; + +/* authentication reason */ +"Open user profiles" = "Отвори потребителските профили"; + +/* No comment provided by engineer. */ +"Open-source protocol and code – anybody can run the servers." = "Протокол и код с отворен код – всеки може да оперира собствени сървъри."; + +/* No comment provided by engineer. */ +"Opening database…" = "Отваряне на база данни…"; + +/* No comment provided by engineer. */ +"Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "Отварянето на линка в браузъра може да намали поверителността и сигурността на връзката. Несигурните SimpleX линкове ще бъдат червени."; + +/* No comment provided by engineer. */ +"or chat with the developers" = "или пишете на разработчиците"; + +/* member role */ +"owner" = "собственик"; + +/* No comment provided by engineer. */ +"Passcode" = "Код за достъп"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "Кодът за достъп е променен!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Въвеждане на код за достъп"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "Кодът за достъп не е променен!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "Кодът за достъп е зададен!"; + +/* No comment provided by engineer. */ +"Password to show" = "Парола за показване"; + +/* No comment provided by engineer. */ +"Paste" = "Постави"; + +/* No comment provided by engineer. */ +"Paste image" = "Постави изображение"; + +/* No comment provided by engineer. */ +"Paste received link" = "Постави получения линк"; + +/* placeholder */ +"Paste the link you received to connect with your contact." = "Поставете линка, който сте получили, за да се свържете с вашия контакт."; + +/* No comment provided by engineer. */ +"peer-to-peer" = "peer-to-peer"; + +/* No comment provided by engineer. */ +"People can connect to you only via the links you share." = "Хората могат да се свържат с вас само чрез ликовете, които споделяте."; + +/* No comment provided by engineer. */ +"Periodically" = "Периодично"; + +/* message decrypt error item */ +"Permanent decryption error" = "Постоянна грешка при декриптиране"; + +/* No comment provided by engineer. */ +"PING count" = "PING бройка"; + +/* No comment provided by engineer. */ +"PING interval" = "PING интервал"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Моля, попитайте вашия контакт, за да активирате изпращане на гласови съобщения."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Моля, проверете мрежовата си връзка с %@ и опитайте отново."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Моля, проверете вашите настройки и тези вашия за контакт."; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Моля, свържете се с груповия администартор."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Моля, въведете правилната текуща парола."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Моля, въведете предишната парола след възстановяване на резервното копие на базата данни. Това действие не може да бъде отменено."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Моля, запомнете го или го съхранявайте на сигурно място - няма начин да възстановите изгубен код за достъп!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Моля, докладвайте го на разработчиците."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Моля, рестартирайте приложението и мигрирайте базата данни, за да активирате push известия."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Моля, съхранявайте паролата на сигурно място, НЯМА да имате достъп до чата, ако я загубите."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите."; + +/* No comment provided by engineer. */ +"Polish interface" = "Полски интерфейс"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "Запазете последната чернова на съобщението с прикачени файлове."; + +/* No comment provided by engineer. */ +"Preset server" = "Предварително зададен сървър"; + +/* No comment provided by engineer. */ +"Preset server address" = "Предварително зададен адрес на сървъра"; + +/* No comment provided by engineer. */ +"Preview" = "Визуализация"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Поверителност и сигурност"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "Поверителността преосмислена"; + +/* No comment provided by engineer. */ +"Private filenames" = "Поверителни имена на файлове"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "Профилни и сървърни връзки"; + +/* No comment provided by engineer. */ +"Profile image" = "Профилно изображение"; + +/* No comment provided by engineer. */ +"Profile password" = "Профилна парола"; + +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "Актуализацията на профила ще бъде изпратена до вашите контакти."; + +/* No comment provided by engineer. */ +"Prohibit audio/video calls." = "Забрани аудио/видео разговорите."; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "Забрани необратимото изтриване на съобщения."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "Забрани реакциите на съобщенията."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "Забрани реакциите на съобщенията."; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Забрани изпращането на лични съобщения до членовете."; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "Забрани изпращането на изчезващи съобщения."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Забрани изпращането на файлове и медия."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "Забрани изпращането на гласови съобщения."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Защити екрана на приложението"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Защитете чат профилите с парола!"; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Време за изчакване на протокола"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Време за изчакване на протокола за KB"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push известия"; + +/* No comment provided by engineer. */ +"Rate the app" = "Оценете приложението"; + +/* chat item menu */ +"React…" = "Реагирай…"; + +/* No comment provided by engineer. */ +"Read" = "Прочетено"; + +/* No comment provided by engineer. */ +"Read more" = "Прочетете още"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Прочетете повече в [Ръководство за потребителя](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Прочетете повече в [Ръководство на потребителя](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; + +/* No comment provided by engineer. */ +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Прочетете повече в нашето [GitHub хранилище](https://github.com/simplex-chat/simplex-chat#readme)."; + +/* No comment provided by engineer. */ +"Read more in our GitHub repository." = "Прочетете повече в нашето хранилище в GitHub."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "Потвърждениeто за доставка е деактивирано"; + +/* No comment provided by engineer. */ +"received answer…" = "получен отговор…"; + +/* No comment provided by engineer. */ +"Received at" = "Получено в"; + +/* copied message info */ +"Received at: %@" = "Получено в: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "получено потвърждение…"; + +/* notification */ +"Received file event" = "Събитие за получен файл"; + +/* message info title */ +"Received message" = "Получено съобщение"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "Получаващият се файл ще бъде спрян."; + +/* No comment provided by engineer. */ +"Receiving via" = "Получаване чрез"; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "Получателите виждат актуализации, докато ги въвеждате."; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Повторно се свържете с всички свързани сървъри, за да принудите доставката на съобщенията. Използва се допълнителен трафик."; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Повторно свърване със сървърите?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Записът е актуализиран на"; + +/* copied message info */ +"Record updated at: %@" = "Записът е актуализиран на: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Намалена консумация на батерията"; + +/* reject incoming call via notification */ +"Reject" = "Отхвърляне"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Отхвърляне (подателят НЕ бива уведомен)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Отхвърли заявката за контакт"; + +/* call status */ +"rejected call" = "отхвърлено повикване"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора."; + +/* No comment provided by engineer. */ +"Remove" = "Премахване"; + +/* No comment provided by engineer. */ +"Remove member" = "Острани член"; + +/* No comment provided by engineer. */ +"Remove member?" = "Острани член?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Премахване на паролата от keychain?"; + +/* No comment provided by engineer. */ +"removed" = "отстранен"; + +/* rcv group event chat item */ +"removed %@" = "отстранен %@"; + +/* rcv group event chat item */ +"removed you" = "ви острани"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Предоговоряне"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Предоговори криптирането"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Предоговори криптирането?"; + +/* chat item action */ +"Reply" = "Отговори"; + +/* No comment provided by engineer. */ +"Required" = "Задължително"; + +/* No comment provided by engineer. */ +"Reset" = "Нулиране"; + +/* No comment provided by engineer. */ +"Reset colors" = "Нулирай цветовете"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Възстановяване на настройките по подразбиране"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Рестартирайте приложението, за да създадете нов чат профил"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Рестартирайте приложението, за да използвате импортирана чат база данни"; + +/* No comment provided by engineer. */ +"Restore" = "Възстанови"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Възстанови резервно копие на база данни"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Възстанови резервно копие на база данни?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Грешка при възстановяване на базата данни"; + +/* chat item action */ +"Reveal" = "Покажи"; + +/* No comment provided by engineer. */ +"Revert" = "Отмени промените"; + +/* No comment provided by engineer. */ +"Revoke" = "Отзови"; + +/* cancel file action */ +"Revoke file" = "Отзови файл"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Отзови файл?"; + +/* No comment provided by engineer. */ +"Role" = "Роля"; + +/* No comment provided by engineer. */ +"Run chat" = "Стартиране на чат"; + +/* chat item action */ +"Save" = "Запази"; + +/* No comment provided by engineer. */ +"Save (and notify contacts)" = "Запази (и уведоми контактите)"; + +/* No comment provided by engineer. */ +"Save and notify contact" = "Запази и уведоми контакта"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "Запази и уведоми членовете на групата"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Запази и актуализирай профила на групата"; + +/* No comment provided by engineer. */ +"Save archive" = "Запази архив"; + +/* No comment provided by engineer. */ +"Save auto-accept settings" = "Запази настройките за автоматично приемане"; + +/* No comment provided by engineer. */ +"Save group profile" = "Запази профила на групата"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Запази паролата и отвори чата"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Запази паролата в Keychain"; + +/* No comment provided by engineer. */ +"Save preferences?" = "Запази настройките?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Запази паролата на профила"; + +/* No comment provided by engineer. */ +"Save servers" = "Запази сървърите"; + +/* No comment provided by engineer. */ +"Save servers?" = "Запази сървърите?"; + +/* No comment provided by engineer. */ +"Save settings?" = "Запази настройките?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Запази съобщението при посрещане?"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "Запазените WebRTC ICE сървъри ще бъдат премахнати"; + +/* No comment provided by engineer. */ +"Scan code" = "Сканирай код"; + +/* No comment provided by engineer. */ +"Scan QR code" = "Сканирай QR код"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Сканирайте кода за сигурност от приложението на вашия контакт."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "Сканирай QR кода на сървъра"; + +/* No comment provided by engineer. */ +"Search" = "Търсене"; + +/* network option */ +"sec" = "сек."; + +/* time unit */ +"seconds" = "секунди"; + +/* No comment provided by engineer. */ +"secret" = "таен"; + +/* server test step */ +"Secure queue" = "Сигурна опашка"; + +/* No comment provided by engineer. */ +"Security assessment" = "Оценка на сигурността"; + +/* No comment provided by engineer. */ +"Security code" = "Код за сигурност"; + +/* chat item text */ +"security code changed" = "кодът за сигурност е променен"; + +/* No comment provided by engineer. */ +"Select" = "Избери"; + +/* No comment provided by engineer. */ +"Self-destruct" = "Самоунищожение"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Код за достъп за самоунищожение"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Кодът за достъп за самоунищожение е променен!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Кодът за достъп за самоунищожение е активиран!"; + +/* No comment provided by engineer. */ +"Send" = "Изпрати"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Изпратете съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Изпращайте потвърждениe за доставка на"; + +/* No comment provided by engineer. */ +"Send direct message" = "Изпрати лично съобщение"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Изпрати изчезващо съобщение"; + +/* No comment provided by engineer. */ +"Send link previews" = "Изпрати визуализация на линковете"; + +/* No comment provided by engineer. */ +"Send live message" = "Изпрати съобщение на живо"; + +/* No comment provided by engineer. */ +"Send notifications" = "Изпращай известия"; + +/* No comment provided by engineer. */ +"Send notifications:" = "Изпратени известия:"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Изпращайте въпроси и идеи"; + +/* No comment provided by engineer. */ +"Send receipts" = "Изпращане на потвърждениe за доставка"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Изпрати от галерия или персонализирани клавиатури."; + +/* No comment provided by engineer. */ +"Sender cancelled file transfer." = "Подателят отмени прехвърлянето на файла."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "Подателят може да е изтрил заявката за връзка."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "Изпращането на файла ще бъде спряно."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "Изпращането на потвърждениe за доставка е деактивирано за %lld контакта"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Изпращането на потвърждениe за доставка е деактивирано за %lld групи"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Изпращането на потвърждениe за доставка е активирано за %lld контакта"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Изпращането на потвърждениe за доставка е активирано за %lld групи"; + +/* No comment provided by engineer. */ +"Sending via" = "Изпращане чрез"; + +/* No comment provided by engineer. */ +"Sent at" = "Изпратено на"; + +/* copied message info */ +"Sent at: %@" = "Изпратено на: %@"; + +/* notification */ +"Sent file event" = "Събитие за изпратен файл"; + +/* message info title */ +"Sent message" = "Изпратено съобщение"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Изпратените съобщения ще бъдат изтрити след зададеното време."; + +/* server test error */ +"Server requires authorization to create queues, check password" = "Сървърът изисква оторизация за създаване на опашки, проверете паролата"; + +/* server test error */ +"Server requires authorization to upload, check password" = "Сървърът изисква оторизация за качване, проверете паролата"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Тестът на сървъра е неуспешен!"; + +/* No comment provided by engineer. */ +"Servers" = "Сървъри"; + +/* No comment provided by engineer. */ +"Set 1 day" = "Задай 1 ден"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Задай име на контакт…"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Задай групови настройки"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Задайте го вместо системната идентификация."; + +/* No comment provided by engineer. */ +"Set passcode" = "Задай kод за достъп"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Задай парола за експортиране"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Задай съобщението, показано на новите членове!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Задай време за изчакване за прокси/VPN"; + +/* No comment provided by engineer. */ +"Settings" = "Настройки"; + +/* chat item action */ +"Share" = "Сподели"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Сподели еднократен линк"; + +/* No comment provided by engineer. */ +"Share address" = "Сподели адрес"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "Сподели адреса с контактите?"; + +/* No comment provided by engineer. */ +"Share link" = "Сподели линк"; + +/* No comment provided by engineer. */ +"Share one-time invitation link" = "Сподели линк за еднократна покана"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Сподели с контактите"; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Показване на обажданията в хронологията на телефона"; + +/* No comment provided by engineer. */ +"Show developer options" = "Покажи опциите за разработчици"; + +/* No comment provided by engineer. */ +"Show last messages" = "Показване на последните съобщения в листа с чатовете"; + +/* No comment provided by engineer. */ +"Show preview" = "Показване на визуализация"; + +/* No comment provided by engineer. */ +"Show:" = "Покажи:"; + +/* No comment provided by engineer. */ +"SimpleX address" = "SimpleX адрес"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "SimpleX Адрес"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "Сигурността на SimpleX Chat беше одитирана от Trail of Bits."; + +/* simplex link type */ +"SimpleX contact address" = "SimpleX адрес за контакт"; + +/* notification */ +"SimpleX encrypted message or connection event" = "SimpleX криптирано съобщение или събитие за връзка"; + +/* simplex link type */ +"SimpleX group link" = "SimpleX групов линк"; + +/* No comment provided by engineer. */ +"SimpleX links" = "SimpleX линкове"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX заключване"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "Режим на SimpleX заключване"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "SimpleX заключване не е активирано!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX заключване е включено"; + +/* simplex link type */ +"SimpleX one-time invitation" = "Еднократна покана за SimpleX"; + +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Опростен режим инкогнито"; + +/* No comment provided by engineer. */ +"Skip" = "Пропускане"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Пропуснати съобщения"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Малки групи (максимум 20)"; + +/* No comment provided by engineer. */ +"SMP servers" = "SMP сървъри"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности."; + +/* notification title */ +"Somebody" = "Някой"; + +/* No comment provided by engineer. */ +"Start a new chat" = "Започни нов чат"; + +/* No comment provided by engineer. */ +"Start chat" = "Започни чат"; + +/* No comment provided by engineer. */ +"Start migration" = "Започни миграция"; + +/* No comment provided by engineer. */ +"starting…" = "стартиране…"; + +/* No comment provided by engineer. */ +"Stop" = "Спри"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "Спрете чата, за да активирате действията с базата данни"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Спри чата?"; + +/* cancel file action */ +"Stop file" = "Спри файл"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Спри получаването на файла?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Спри изпращането на файла?"; + +/* No comment provided by engineer. */ +"Stop sharing" = "Спри споделянето"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "Спри споделянето на адреса?"; + +/* authentication reason */ +"Stop SimpleX" = "Спри SimpleX"; + +/* No comment provided by engineer. */ +"strike" = "зачеркнат"; + +/* No comment provided by engineer. */ +"Submit" = "Изпрати"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "Подкрепете SimpleX Chat"; + +/* No comment provided by engineer. */ +"System" = "Системен"; + +/* No comment provided by engineer. */ +"System authentication" = "Системна идентификация"; + +/* No comment provided by engineer. */ +"Take picture" = "Направи снимка"; + +/* No comment provided by engineer. */ +"Tap button " = "Докосни бутона "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Докосни за активиране на профил."; + +/* No comment provided by engineer. */ +"Tap to join" = "Докосни за вход"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "Докосни за инкогнито вход"; + +/* No comment provided by engineer. */ +"Tap to start a new chat" = "Докосни за започване на нов чат"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "Времето на изчакване за установяване на TCP връзка"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* server test failure */ +"Test failed at step %@." = "Тестът е неуспешен на стъпка %@."; + +/* No comment provided by engineer. */ +"Test server" = "Тествай сървър"; + +/* No comment provided by engineer. */ +"Test servers" = "Тествай сървърите"; + +/* No comment provided by engineer. */ +"Tests failed!" = "Тестовете са неуспешни!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "Благодарим Ви, че инсталирахте SimpleX Chat!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Благодарение на потребителите – [допринесете през Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Благодарение на потребителите – допринесете през Weblate!"; + +/* No comment provided by engineer. */ +"The 1st platform without any user identifiers – private by design." = "Първата платформа без никакви потребителски идентификатори – поверителна по дизайн."; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Опитът за промяна на паролата на базата данни не беше завършен."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Връзката, която приехте, ще бъде отказана!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "Контактът, с когото споделихте този линк, НЯМА да може да се свърже!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Създаденият архив е достъпен чрез Настройки на приложението / База данни / Стар архив на база данни."; + +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!"; + +/* No comment provided by engineer. */ +"The group is fully decentralized – it is visible only to the members." = "Групата е напълно децентрализирана – видима е само за членовете."; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "Хешът на предишното съобщение е различен."; + +/* No comment provided by engineer. */ +"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Неправилно ID на следващото съобщение (по-малко или еднакво с предишното).\nТова може да се случи поради някаква грешка или когато връзката е компрометирана."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Съобщението ще бъде изтрито за всички членове."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Съобщението ще бъде маркирано като модерирано за всички членове."; + +/* No comment provided by engineer. */ +"The next generation of private messaging" = "Ново поколение поверителни съобщения"; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Старата база данни не бе премахната по време на миграцията, тя може да бъде изтрита."; + +/* No comment provided by engineer. */ +"The profile is only shared with your contacts." = "Профилът се споделя само с вашите контакти."; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Втората отметка, която пропуснахме! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "Подателят НЯМА да бъде уведомен"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "Сървърите за нови връзки на текущия ви чат профил **%@**."; + +/* No comment provided by engineer. */ +"Theme" = "Тема"; + +/* No comment provided by engineer. */ +"There should be at least one user profile." = "Трябва да има поне един потребителски профил."; + +/* No comment provided by engineer. */ +"There should be at least one visible user profile." = "Трябва да има поне един видим потребителски профил."; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Тези настройки са за текущия ви профил **%@**."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Те могат да бъдат променени в настройките за всеки контакт и група."; + +/* No comment provided by engineer. */ +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени."; + +/* No comment provided by engineer. */ +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Това действие не може да бъде отменено - съобщенията, изпратени и получени по-рано от избраното, ще бъдат изтрити. Може да отнеме няколко минути."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени."; + +/* notification title */ +"this contact" = "този контакт"; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Тази група има над %lld членове, потвърждения за доставка не се изпращат."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Тази група вече не съществува."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Тази настройка се прилага за съобщения в текущия ви профил **%@**."; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "За да задавате въпроси и да получавате актуализации:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението."; + +/* No comment provided by engineer. */ +"To make a new connection" = "За да направите нова връзка"; + +/* No comment provided by engineer. */ +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти."; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "За да не се разкрива часовата зона, файловете с изображения/глас използват UTC."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "За да защитите информацията си, включете SimpleX заключване.\nЩе бъдете подканени да извършите идентификация, преди тази функция да бъде активирана."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "За да запишете гласово съобщение, моля, дайте разрешение за използване на микрофон."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "За да разкриете своя скрит профил, въведете пълна парола в полето за търсене на страницата **Вашите чат профили**."; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "За поддръжка на незабавни push известия, базата данни за чат трябва да бъде мигрирана."; + +/* No comment provided by engineer. */ +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства."; + +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Избор на инкогнито при свързване."; + +/* No comment provided by engineer. */ +"Transport isolation" = "Транспортна изолация"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Опит за свързване със сървъра, използван за получаване на съобщения от този контакт."; + +/* No comment provided by engineer. */ +"Turn off" = "Изключи"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "Изключи известията?"; + +/* No comment provided by engineer. */ +"Turn on" = "Включи"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Не може да се запише гласово съобщение"; + +/* item status description */ +"Unexpected error: %@" = "Неочаквана грешка: %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Неочаквано състояние на миграция"; + +/* No comment provided by engineer. */ +"Unfav." = "Премахни от любимите"; + +/* No comment provided by engineer. */ +"Unhide" = "Покажи"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Покажи чат профила"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Покажи профила"; + +/* No comment provided by engineer. */ +"Unit" = "Мерна единица"; + +/* connection info */ +"unknown" = "неизвестен"; + +/* callkit banner */ +"Unknown caller" = "Неизвестен номер"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Неизвестна грешка в базата данни: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Непозната грешка"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Освен ако не използвате интерфейса за повикване на iOS, активирайте режима \"Не безпокой\", за да избегнете прекъсвания."; + +/* No comment provided by engineer. */ +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте.\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка."; + +/* No comment provided by engineer. */ +"Unlock" = "Отключи"; + +/* authentication reason */ +"Unlock app" = "Отключи приложението"; + +/* No comment provided by engineer. */ +"Unmute" = "Уведомявай"; + +/* No comment provided by engineer. */ +"Unread" = "Непрочетено"; + +/* No comment provided by engineer. */ +"Update" = "Актуализация"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "Актуализиране на настройката за .onion хостове?"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Актуализирай паролата на базата данни"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Актуализиране на мрежовите настройки?"; + +/* No comment provided by engineer. */ +"Update transport isolation mode?" = "Актуализиране на режима на изолация на транспорта?"; + +/* rcv group event chat item */ +"updated group profile" = "актуализиран профил на групата"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "Актуализирането на настройките ще свърже отново клиента към всички сървъри."; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "Актуализирането на тази настройка ще свърже повторно клиента към всички сървъри."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Актуализирай и отвори чата"; + +/* server test step */ +"Upload file" = "Качи файл"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Използвай .onion хостове"; + +/* No comment provided by engineer. */ +"Use chat" = "Използвай чата"; + +/* No comment provided by engineer. */ +"Use current profile" = "Използвай текущия профил"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Използвай за нови връзки"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "Използвай интерфейса за повикване на iOS"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Използвай нов инкогнито профил"; + +/* No comment provided by engineer. */ +"Use server" = "Използвай сървър"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "Използвай сървърите на SimpleX Chat?"; + +/* No comment provided by engineer. */ +"User profile" = "Потребителски профил"; + +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = "Използването на .onion хостове изисква съвместим VPN доставчик."; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Използват се сървърите на SimpleX Chat."; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Потвръди сигурността на връзката"; + +/* No comment provided by engineer. */ +"Verify security code" = "Потвръди кода за сигурност"; + +/* No comment provided by engineer. */ +"Via browser" = "Чрез браузър"; + +/* chat list item description */ +"via contact address link" = "чрез линк с адрес за контакт"; + +/* chat list item description */ +"via group link" = "чрез групов линк"; + +/* chat list item description */ +"via one-time link" = "чрез еднократен линк за връзка"; + +/* No comment provided by engineer. */ +"via relay" = "чрез реле"; + +/* No comment provided by engineer. */ +"Video call" = "Видео разговор"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "видео разговор (не е e2e криптиран)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "Видеото ще бъде получено, когато вашият контакт завърши качването му."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "Видеото ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "Видео и файлове до 1gb"; + +/* No comment provided by engineer. */ +"View security code" = "Виж кода за сигурност"; + +/* No comment provided by engineer. */ +"Voice message…" = "Гласово съобщение…"; + +/* chat feature */ +"Voice messages" = "Гласови съобщения"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "Гласовите съобщения са забранени в този чат."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this group." = "Гласовите съобщения са забранени в тази група."; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Гласовите съобщения са забранени!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "чака се отговор…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "чака се за потвърждение…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "Изчаква се получаването на файла"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Изчаква се получаването на изображението"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Изчаква се получаването на видеото"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "иска да се свърже с вас!"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Предупреждение: Може да загубите някои данни!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "WebRTC ICE сървъри"; + +/* time unit */ +"weeks" = "седмици"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Добре дошли %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Съобщение при посрещане"; + +/* No comment provided by engineer. */ +"What's new" = "Какво е новото"; + +/* No comment provided by engineer. */ +"When available" = "Когато са налични"; + +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите."; + +/* No comment provided by engineer. */ +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "С незадължително съобщение при посрещане."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Грешна парола за базата данни"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Грешна парола!"; + +/* No comment provided by engineer. */ +"XFTP servers" = "XFTP сървъри"; + +/* pref value */ +"yes" = "да"; + +/* No comment provided by engineer. */ +"You" = "Вие"; + +/* No comment provided by engineer. */ +"You accepted connection" = "Вие приехте връзката"; + +/* No comment provided by engineer. */ +"You allow" = "Вие позволявате"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Вече имате чат профил със същото показвано име. Моля, изберете друго име."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Вече сте вече свързани с %@."; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт."; + +/* No comment provided by engineer. */ +"you are invited to group" = "вие сте поканени в групата"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Поканени сте в групата"; + +/* No comment provided by engineer. */ +"you are observer" = "вие сте наблюдател"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението."; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона **Отваряне в мобилно приложение**."; + +/* No comment provided by engineer. */ +"You can create it later" = "Можете да го създадете по-късно"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Можете да активирате по-късно през Настройки"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Можете да скриете или заглушите известията за потребителски профил - плъзнете надясно."; + +/* notification body */ +"You can now send messages to %@" = "Вече можете да изпращате съобщения до %@"; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "Можете да зададете визуализация на известията на заключен екран през настройките."; + +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Можете да споделите линк или QR код - всеки ще може да се присъедини към групата. Няма да загубите членовете на групата, ако по-късно я изтриете."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с **%@**."; + +/* No comment provided by engineer. */ +"You can share your address as a link or QR code - anybody can connect to you." = "Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението"; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "Можете да включите SimpleX заключване през Настройки."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Можете да използвате markdown за форматиране на съобщенията:"; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Не може да изпращате съобщения!"; + +/* chat item text */ +"you changed address" = "променихте адреса"; + +/* chat item text */ +"you changed address for %@" = "променихте адреса за %@"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "променихте ролята си на %@"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "променихте ролята на %1$@ на %2$@"; + +/* No comment provided by engineer. */ +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Вие контролирате през кой сървър(и) **да получавате** съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Не можахте да бъдете потвърдени; Моля, опитайте отново."; + +/* No comment provided by engineer. */ +"You have no chats" = "Нямате чатове"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Вие поканихте контакта"; + +/* No comment provided by engineer. */ +"You joined this group" = "Вие се присъединихте към тази група"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Вие се присъединихте към тази група. Свързване с поканващия член на групата."; + +/* snd group event chat item */ +"you left" = "вие напуснахте"; + +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Трябва да използвате най-новата версия на вашата чат база данни САМО на едно устройство, в противен случай може да спрете да получавате съобщения от някои контакти."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Отхвърлихте поканата за групата"; + +/* snd group event chat item */ +"you removed %@" = "премахнахте %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Изпратихте покана за групата"; + +/* chat list item description */ +"you shared one-time link" = "споделихте еднократен линк за връзка"; + +/* chat list item description */ +"you shared one-time link incognito" = "споделихте еднократен инкогнито линк за връзка"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим."; + +/* No comment provided by engineer. */ +"You will join a group this link refers to and connect to its group members." = "Ще се присъедините към групата, към която този линк препраща, и ще се свържете с нейните членове."; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "Все още ще получавате обаждания и известия от заглушени профили, когато са активни."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "Ще спрете да получавате съобщения от тази група. Историята на чата ще бъде запазена."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Няма да загубите контактите си, ако по-късно изтриете адреса си."; + +/* No comment provided by engineer. */ +"you: " = "вие: "; + +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Опитвате се да поканите контакт, с когото сте споделили инкогнито профил, в групата, в която използвате основния си профил"; + +/* No comment provided by engineer. */ +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени"; + +/* No comment provided by engineer. */ +"Your %@ servers" = "Вашите %@ сървъри"; + +/* No comment provided by engineer. */ +"Your calls" = "Вашите обаждания"; + +/* No comment provided by engineer. */ +"Your chat database" = "Вашата чат база данни"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "Вашата чат база данни не е криптирана - задайте парола, за да я криптирате."; + +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "Вашият чат профил ще бъде изпратен на членовете на групата"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Вашите чат профили"; + +/* No comment provided by engineer. */ +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Вашият контакт трябва да бъде онлайн, за да осъществите връзката.\nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк)."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@)."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "Вашите контакти могат да позволят пълното изтриване на съобщението."; + +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Вашите контакти в SimpleX ще го видят.\nМожете да го промените в Настройки."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "Вашите контакти ще останат свързани."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната."; + +/* No comment provided by engineer. */ +"Your current profile" = "Вашият текущ профил"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "Вашите ICE сървъри"; + +/* No comment provided by engineer. */ +"Your preferences" = "Вашите настройки"; + +/* No comment provided by engineer. */ +"Your privacy" = "Вашата поверителност"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Вашият профил **%@** ще бъде споделен."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти.\nSimpleX сървърите не могат да видят вашия профил."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство."; + +/* No comment provided by engineer. */ +"Your random profile" = "Вашият автоматично генериран профил"; + +/* No comment provided by engineer. */ +"Your server" = "Вашият сървър"; + +/* No comment provided by engineer. */ +"Your server address" = "Вашият адрес на сървъра"; + +/* No comment provided by engineer. */ +"Your settings" = "Вашите настройки"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "Вашият SimpleX адрес"; + +/* No comment provided by engineer. */ +"Your SMP servers" = "Вашите SMP сървъри"; + +/* No comment provided by engineer. */ +"Your XFTP servers" = "Вашите XFTP сървъри"; + diff --git a/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..d85455d875 --- /dev/null +++ b/apps/ios/bg.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,15 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX се нуждае от достъп до камерата, за да сканира QR кодове, за да се свърже с други потребители и за видео разговори."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX използва Face ID за локалнa идентификация"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX се нуждае от достъп до микрофона за аудио и видео разговори и за запис на гласови съобщения."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX се нуждае от достъп до фотобиблиотека за запазване на заснета и получена медия"; + diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index d3c165ab0d..3d7d5f8fe2 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -88,6 +88,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*tučně*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Historie"; + +/* copied message info */ +"## In reply to" = "## Odpovídáno"; + /* No comment provided by engineer. */ "#secret#" = "#tajný#"; @@ -106,6 +115,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ a %@ připojen"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ na %2$@:"; @@ -124,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "%@ se chce připojit!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ a %lld ostatní členové připojeni"; + /* copied message info */ "%@:" = "%@:"; @@ -245,10 +260,7 @@ "A new contact" = "Nový kontakt"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Náhodný profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Vašemu kontaktu bude zaslán náhodný profil"; +"A new random profile will be shared." = "Nový náhodný profil bude sdílen."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Samostatné připojení TCP bude použito **pro každý chat profil, který máte v aplikaci**."; @@ -285,12 +297,12 @@ "Accept" = "Přijmout"; /* No comment provided by engineer. */ -"Accept contact" = "Přijmout kontakt"; +"Accept connection request?" = "Přijmout kontakt?"; /* notification body */ "Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Přijmout inkognito"; /* call status */ @@ -696,11 +708,17 @@ /* server test step */ "Connect" = "Připojit"; +/* No comment provided by engineer. */ +"Connect directly" = "Připojit přímo"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Spojit se inkognito"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "připojit se k vývojářům SimpleX Chat."; /* No comment provided by engineer. */ -"Connect via contact link?" = "Připojit se přes kontaktní odkaz?"; +"Connect via contact link" = "Připojit se přes odkaz"; /* No comment provided by engineer. */ "Connect via group link?" = "Připojit se přes odkaz skupiny?"; @@ -712,7 +730,7 @@ "Connect via link / QR code" = "Připojit se prostřednictvím odkazu / QR kódu"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Připojit se jednorázovým odkazem?"; +"Connect via one-time link" = "Připojit se jednorázovým odkazem"; /* No comment provided by engineer. */ "connected" = "připojeno"; @@ -756,9 +774,6 @@ /* chat list item title (it should not be shown */ "connection established" = "spojení navázáno"; -/* No comment provided by engineer. */ -"Connection request" = "Žádost o připojení"; - /* No comment provided by engineer. */ "Connection request sent!" = "Požadavek na připojení byl odeslán!"; @@ -936,6 +951,12 @@ /* pref value */ "default (%@)" = "výchozí (%@)"; +/* No comment provided by engineer. */ +"default (no)" = "výchozí (ne)"; + +/* No comment provided by engineer. */ +"default (yes)" = "výchozí (ano)"; + /* chat item action */ "Delete" = "Smazat"; @@ -1053,6 +1074,9 @@ /* rcv group event chat item */ "deleted group" = "odstraněna skupina"; +/* No comment provided by engineer. */ +"Delivery" = "Doručenka"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "Potvrzení o doručení jsou vypnuté!"; @@ -1404,6 +1428,9 @@ /* No comment provided by engineer. */ "Error sending message" = "Chyba při odesílání zprávy"; +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Chyba nastavování potvrzení o doručení!"; + /* No comment provided by engineer. */ "Error starting chat" = "Chyba při spuštění chatu"; @@ -1528,7 +1555,7 @@ "Full link" = "Úplný odkaz"; /* No comment provided by engineer. */ -"Full name (optional)" = "Celé jméno (volitelné)"; +"Full name (optional)" = "Celé jméno (volitelně)"; /* No comment provided by engineer. */ "Full name:" = "Celé jméno:"; @@ -1641,7 +1668,7 @@ /* No comment provided by engineer. */ "Hide:" = "Skrýt:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Historie"; /* time unit */ @@ -1710,7 +1737,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Vylepšená konfigurace serveru"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "V odpovědi na"; /* No comment provided by engineer. */ @@ -1720,10 +1747,7 @@ "Incognito mode" = "Režim inkognito"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "Zde není podporován režim inkognito - členům skupiny bude zaslán váš hlavní profil"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Režim inkognito chrání soukromí vašeho hlavního profilového jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt."; /* chat list item description */ "incognito via contact address link" = "inkognito přes odkaz na kontaktní adresu"; @@ -1788,6 +1812,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Neplatná adresa serveru!"; +/* item status text */ +"Invalid status" = "Neplatný status"; + /* No comment provided by engineer. */ "Invitation expired!" = "Platnost pozvánky vypršela!"; @@ -1861,10 +1888,10 @@ "Join group" = "Připojit ke skupině"; /* No comment provided by engineer. */ -"Join incognito" = "Připojte se inkognito"; +"Join incognito" = "Připojit se inkognito"; /* No comment provided by engineer. */ -"Joining group" = "Připojení ke skupině"; +"Joining group" = "Připojování ke skupině"; /* No comment provided by engineer. */ "Keep your connections" = "Zachovat vaše připojení"; @@ -1977,7 +2004,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Člen bude odstraněn ze skupiny - toto nelze vzít zpět!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Chyba doručení zprávy"; /* No comment provided by engineer. */ @@ -2049,6 +2076,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Další vylepšení se chystají již brzy!"; +/* item status description */ +"Most likely this connection is deleted." = "Pravděpodobně je toto spojení smazáno."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Tento kontakt s největší pravděpodobností smazal spojení s vámi."; @@ -2293,8 +2323,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Vložení přijatého odkazu"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Vložte odkaz, který jste obdrželi, do pole níže a spojte se se svým kontaktem."; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; @@ -2422,12 +2452,18 @@ /* No comment provided by engineer. */ "Protocol timeout" = "Časový limit protokolu"; +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Časový limit protokolu na KB"; + /* No comment provided by engineer. */ "Push notifications" = "Nabízená oznámení"; /* No comment provided by engineer. */ "Rate the app" = "Ohodnoťte aplikaci"; +/* chat item menu */ +"React…" = "Reagovat…"; + /* No comment provided by engineer. */ "Read" = "Číst"; @@ -2464,6 +2500,9 @@ /* message info title */ "Received message" = "Přijatá zpráva"; +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele."; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "Příjem souboru bude zastaven."; @@ -2473,6 +2512,12 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Příjemci uvidí aktualizace během jejich psaní."; +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Znovu připojte všechny připojené servery a vynuťte doručení zprávy. Využívá další provoz."; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Znovu připojit servery?"; + /* No comment provided by engineer. */ "Record updated at" = "Záznam aktualizován v"; @@ -2486,7 +2531,7 @@ "Reject" = "Odmítnout"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Odmítnout kontakt (odesílatel NEBUDE upozorněn)"; +"Reject (sender NOT notified)" = "Odmítnout kontakt (odesílatel NEBUDE upozorněn)"; /* No comment provided by engineer. */ "Reject contact request" = "Odmítnout žádost o kontakt"; @@ -2692,6 +2737,9 @@ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "Poslat živou zprávu - zpráva se bude aktualizovat pro příjemce během psaní"; +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Potvrzení o doručení zasílat na"; + /* No comment provided by engineer. */ "Send direct message" = "Odeslat přímou zprávu"; @@ -3034,6 +3082,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Profil je sdílen pouze s vašimi kontakty."; +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Druhé zaškrtnutí jsme přehlédli! ✅"; + /* No comment provided by engineer. */ "The sender will NOT be notified" = "Odesílatel NEBUDE informován"; @@ -3053,7 +3104,7 @@ "These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Mohou být přepsány v nastavení kontaktů"; +"They can be overridden in contact and group settings." = "Mohou být přepsány v nastavení kontaktů."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Tuto akci nelze vrátit zpět - všechny přijaté a odeslané soubory a média budou smazány. Obrázky s nízkým rozlišením zůstanou zachovány."; @@ -3079,9 +3130,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Pro připojení může váš kontakt naskenovat QR kód, nebo použít odkaz v aplikaci."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části chatu."; - /* No comment provided by engineer. */ "To make a new connection" = "Vytvoření nového připojení"; @@ -3127,7 +3175,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Nelze nahrát hlasovou zprávu"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Neočekávaná chyba: %@"; /* No comment provided by engineer. */ @@ -3449,7 +3497,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení."; /* No comment provided by engineer. */ -"You invited your contact" = "Pozvali jste svůj kontakt"; +"You invited a contact" = "Pozvali jste svůj kontakt"; /* No comment provided by engineer. */ "You joined this group" = "Připojili jste se k této skupině"; @@ -3529,9 +3577,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Váš chat profil bude zaslán členům skupiny"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Váš chat profil bude odeslán vašemu kontaktu"; - /* No comment provided by engineer. */ "Your chat profiles" = "Vaše chat profily"; @@ -3568,9 +3613,6 @@ /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Váš profil je uložen ve vašem zařízení a sdílen pouze s vašimi kontakty.\nServery SimpleX nevidí váš profil."; -/* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Váš profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz"; - /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Váš profil, kontakty a doručené zprávy jsou uloženy ve vašem zařízení."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 7c87b752db..111ce0d916 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -88,6 +88,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*fett*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Vergangenheit"; + +/* copied message info */ +"## In reply to" = "## Als Antwort auf"; + /* No comment provided by engineer. */ "#secret#" = "#geheim#"; @@ -106,6 +115,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ und %@ wurden verbunden"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ an %2$@:"; @@ -124,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "%@ will sich mit Ihnen verbinden!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ und %lld weitere Mitglieder wurden verbunden"; + /* copied message info */ "%@:" = "%@:"; @@ -245,10 +260,7 @@ "A new contact" = "Ein neuer Kontakt"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Ein zufälliges Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Ein zufälliges Profil wird an Ihren Kontakt gesendet"; +"A new random profile will be shared." = "Es wird ein neues Zufallsprofil geteilt."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "**Für jedes von Ihnen in der App genutzte Chat-Profil** wird eine separate TCP-Verbindung genutzt."; @@ -285,12 +297,12 @@ "Accept" = "Annehmen"; /* No comment provided by engineer. */ -"Accept contact" = "Kontakt annehmen"; +"Accept connection request?" = "Kontaktanfrage annehmen?"; /* notification body */ "Accept contact request from %@?" = "Die Kontaktanfrage von %@ annehmen?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Inkognito akzeptieren"; /* call status */ @@ -345,7 +357,7 @@ "All chats and messages will be deleted - this cannot be undone!" = "Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald diese eingegeben wird."; +"All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird."; /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; @@ -363,16 +375,16 @@ "Allow" = "Erlauben"; /* No comment provided by engineer. */ -"Allow calls only if your contact allows them." = "Anrufe sind nur erlaubt, wenn Ihr Kontakt das ebenfalls erlaubt."; +"Allow calls only if your contact allows them." = "Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Verschwindende Nachrichten nur erlauben, wenn Ihr Kontakt das ebenfalls erlaubt."; +"Allow disappearing messages only if your contact allows it to you." = "Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you." = "Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Reaktionen auf Nachrichten sind nur möglich, falls Ihr Kontakt dies erlaubt."; +"Allow message reactions only if your contact allows them." = "Erlauben Sie Reaktionen auf Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ "Allow message reactions." = "Reaktionen auf Nachrichten erlauben."; @@ -393,7 +405,7 @@ "Allow to send voice messages." = "Das Senden von Sprachnachrichten erlauben."; /* No comment provided by engineer. */ -"Allow voice messages only if your contact allows them." = "Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt."; +"Allow voice messages only if your contact allows them." = "Erlauben Sie Sprachnachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; /* No comment provided by engineer. */ "Allow voice messages?" = "Sprachnachricht erlauben?"; @@ -432,7 +444,7 @@ "App build: %@" = "App Build: %@"; /* No comment provided by engineer. */ -"App icon" = "App Icon"; +"App icon" = "App-Icon"; /* No comment provided by engineer. */ "App passcode" = "App-Zugangscode"; @@ -462,10 +474,10 @@ "audio call (not e2e encrypted)" = "Audioanruf (nicht E2E verschlüsselt)"; /* chat feature */ -"Audio/video calls" = "Audio/Video Anrufe"; +"Audio/video calls" = "Audio-/Video-Anrufe"; /* No comment provided by engineer. */ -"Audio/video calls are prohibited." = "Audio/Video Anrufe sind nicht erlaubt."; +"Audio/video calls are prohibited." = "Audio-/Video-Anrufe sind nicht erlaubt."; /* PIN entry */ "Authentication cancelled" = "Authentifizierung abgebrochen"; @@ -631,7 +643,7 @@ "Chat is stopped" = "Der Chat ist beendet"; /* No comment provided by engineer. */ -"Chat preferences" = "Chat Präferenzen"; +"Chat preferences" = "Chat-Präferenzen"; /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -697,10 +709,16 @@ "Connect" = "Verbinden"; /* No comment provided by engineer. */ -"connect to SimpleX Chat developers." = "Mit den SimpleX Chat Entwicklern verbinden."; +"Connect directly" = "Direkt verbinden"; /* No comment provided by engineer. */ -"Connect via contact link?" = "Über den Kontakt-Link verbinden?"; +"Connect incognito" = "Inkognito verbinden"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "Mit den SimpleX Chat-Entwicklern verbinden."; + +/* No comment provided by engineer. */ +"Connect via contact link" = "Über den Kontakt-Link verbinden"; /* No comment provided by engineer. */ "Connect via group link?" = "Über den Gruppen-Link verbinden?"; @@ -712,7 +730,7 @@ "Connect via link / QR code" = "Über einen Link / QR-Code verbinden"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Über einen Einmal-Link verbinden?"; +"Connect via one-time link" = "Über einen Einmal-Link verbinden"; /* No comment provided by engineer. */ "connected" = "Verbunden"; @@ -730,7 +748,7 @@ "connecting (introduced)" = "Verbindung (erstellt)"; /* No comment provided by engineer. */ -"connecting (introduction invitation)" = "Verbindung (eingeladen)"; +"connecting (introduction invitation)" = "Verbinde (nach einer Einladung)"; /* call status */ "connecting call" = "Anruf wird verbunden…"; @@ -756,9 +774,6 @@ /* chat list item title (it should not be shown */ "connection established" = "Verbindung hergestellt"; -/* No comment provided by engineer. */ -"Connection request" = "Verbindungsanfrage"; - /* No comment provided by engineer. */ "Connection request sent!" = "Verbindungsanfrage wurde gesendet!"; @@ -796,7 +811,7 @@ "Contact name" = "Kontaktname"; /* No comment provided by engineer. */ -"Contact preferences" = "Kontakt Präferenzen"; +"Contact preferences" = "Kontakt-Präferenzen"; /* No comment provided by engineer. */ "Contacts" = "Kontakte"; @@ -829,7 +844,7 @@ "Create link" = "Link erzeugen"; /* No comment provided by engineer. */ -"Create one-time invitation link" = "Erstellen Sie einen einmaligen Einladungslink"; +"Create one-time invitation link" = "Einmal-Einladungslink erstellen"; /* server test step */ "Create queue" = "Erzeuge Warteschlange"; @@ -1060,10 +1075,13 @@ "deleted group" = "Gruppe gelöscht"; /* No comment provided by engineer. */ -"Delivery receipts are disabled!" = "Zustellungs-Quittierungen sind deaktiviert!"; +"Delivery" = "Zustellung"; /* No comment provided by engineer. */ -"Delivery receipts!" = "Zustellungs-Quittierungen!"; +"Delivery receipts are disabled!" = "Empfangsbestätigungen sind deaktiviert!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Empfangsbestätigungen!"; /* No comment provided by engineer. */ "Description" = "Beschreibung"; @@ -1078,10 +1096,10 @@ "Device" = "Gerät"; /* No comment provided by engineer. */ -"Device authentication is disabled. Turning off SimpleX Lock." = "Die Geräteauthentifizierung ist deaktiviert. SimpleX Sperre ist abgeschaltet."; +"Device authentication is disabled. Turning off SimpleX Lock." = "Die Geräteauthentifizierung ist deaktiviert. SimpleX-Sperre ist abgeschaltet."; /* No comment provided by engineer. */ -"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Die Geräteauthentifizierung ist deaktiviert. Sie können die SimpleX Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben."; +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Die Geräteauthentifizierung ist deaktiviert. Sie können die SimpleX-Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben."; /* No comment provided by engineer. */ "different migration in the app/database: %@ / %@" = "Unterschiedlicher Migrationsstand in der App/Datenbank: %@ / %@"; @@ -1105,7 +1123,10 @@ "Disable for all" = "Für Alle deaktivieren"; /* authentication reason */ -"Disable SimpleX Lock" = "SimpleX Sperre deaktivieren"; +"Disable SimpleX Lock" = "SimpleX-Sperre deaktivieren"; + +/* No comment provided by engineer. */ +"disabled" = "deaktiviert"; /* No comment provided by engineer. */ "Disappearing message" = "Verschwindende Nachricht"; @@ -1204,7 +1225,7 @@ "Enable self-destruct passcode" = "Selbstzerstörungs-Zugangscode aktivieren"; /* authentication reason */ -"Enable SimpleX Lock" = "SimpleX Sperre aktivieren"; +"Enable SimpleX Lock" = "SimpleX-Sperre aktivieren"; /* No comment provided by engineer. */ "Enable TCP keep-alive" = "TCP-Keep-alive aktivieren"; @@ -1224,6 +1245,9 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Datenbank verschlüsseln?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Lokale Dateien verschlüsseln"; + /* No comment provided by engineer. */ "Encrypted database" = "Verschlüsselte Datenbank"; @@ -1335,6 +1359,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Fehler beim Erstellen des Profils!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Fehler beim Entschlüsseln der Datei"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Fehler beim Löschen der Chat-Datenbank"; @@ -1360,7 +1387,7 @@ "Error deleting user profile" = "Fehler beim Löschen des Benutzerprofils"; /* No comment provided by engineer. */ -"Error enabling delivery receipts!" = "Fehler beim Aktivieren der Empfangsbestätigungen!"; +"Error enabling delivery receipts!" = "Fehler beim Aktivieren von Empfangsbestätigungen!"; /* No comment provided by engineer. */ "Error enabling notifications" = "Fehler beim Aktivieren der Benachrichtigungen"; @@ -1411,7 +1438,7 @@ "Error sending message" = "Fehler beim Senden der Nachricht"; /* No comment provided by engineer. */ -"Error setting delivery receipts!" = "Fehler beim Setzen der Empfangsbestätigungen!"; +"Error setting delivery receipts!" = "Fehler beim Setzen von Empfangsbestätigungen!"; /* No comment provided by engineer. */ "Error starting chat" = "Fehler beim Starten des Chats"; @@ -1452,6 +1479,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; +/* No comment provided by engineer. */ +"event happened" = "event happened"; + /* No comment provided by engineer. */ "Exit without saving" = "Beenden ohne Speichern"; @@ -1573,7 +1603,7 @@ "Group invitation expired" = "Die Gruppeneinladung ist abgelaufen"; /* No comment provided by engineer. */ -"Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt."; +"Group invitation is no longer valid, it was removed by sender." = "Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde."; /* No comment provided by engineer. */ "Group link" = "Gruppen-Link"; @@ -1606,7 +1636,7 @@ "Group moderation" = "Gruppenmoderation"; /* No comment provided by engineer. */ -"Group preferences" = "Gruppenpräferenzen"; +"Group preferences" = "Gruppen-Präferenzen"; /* No comment provided by engineer. */ "Group profile" = "Gruppenprofil"; @@ -1650,7 +1680,7 @@ /* No comment provided by engineer. */ "Hide:" = "Verberge:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Vergangenheit"; /* time unit */ @@ -1719,20 +1749,17 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Verbesserte Serverkonfiguration"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "Als Antwort auf"; /* No comment provided by engineer. */ "Incognito" = "Inkognito"; /* No comment provided by engineer. */ -"Incognito mode" = "Inkognito Modus"; +"Incognito mode" = "Inkognito-Modus"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Der Inkognito-Modus schützt die Privatsphäre Ihres Hauptprofilnamens und -bildes – für jeden neuen Kontakt wird ein neues Zufallsprofil erstellt."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Der Inkognito-Modus schützt Ihre Privatsphäre, indem für jeden Kontakt ein neues Zufallsprofil erstellt wird."; /* chat list item description */ "incognito via contact address link" = "Inkognito über einen Kontaktadressen-Link"; @@ -1797,6 +1824,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Ungültige Serveradresse!"; +/* item status text */ +"Invalid status" = "Ungültiger Status"; + /* No comment provided by engineer. */ "Invitation expired!" = "Einladung abgelaufen!"; @@ -1986,7 +2016,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Fehler bei der Nachrichtenzustellung"; /* No comment provided by engineer. */ @@ -2058,6 +2088,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Weitere Verbesserungen sind bald verfügbar!"; +/* item status description */ +"Most likely this connection is deleted." = "Wahrscheinlich ist diese Verbindung gelöscht worden."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht."; @@ -2130,6 +2163,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Keine Kontakte zum Hinzufügen"; +/* No comment provided by engineer. */ +"No delivery information" = "Keine Information über die Zustellung"; + /* No comment provided by engineer. */ "No device token!" = "Kein Geräte-Token!"; @@ -2210,7 +2246,7 @@ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; /* No comment provided by engineer. */ -"Only group owners can change group preferences." = "Gruppenpräferenzen können nur von Gruppen-Eigentümern geändert werden."; +"Only group owners can change group preferences." = "Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden."; /* No comment provided by engineer. */ "Only group owners can enable files and media." = "Nur Gruppenbesitzer können Dateien und Medien aktivieren."; @@ -2302,8 +2338,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Fügen Sie den erhaltenen Link ein"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Um sich mit Ihrem Kontakt zu verbinden, fügen Sie den erhaltenen Link in das Feld unten ein."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Um sich mit Ihrem Kontakt zu verbinden, fügen Sie den erhaltenen Link in das Feld unten ein."; /* No comment provided by engineer. */ "peer-to-peer" = "Peer-to-Peer"; @@ -2318,7 +2354,7 @@ "Permanent decryption error" = "Entschlüsselungsfehler"; /* No comment provided by engineer. */ -"PING count" = "PING Zähler"; +"PING count" = "PING-Zähler"; /* No comment provided by engineer. */ "PING interval" = "PING-Intervall"; @@ -2399,7 +2435,7 @@ "Profile update will be sent to your contacts." = "Profil-Aktualisierung wird an Ihre Kontakte gesendet."; /* No comment provided by engineer. */ -"Prohibit audio/video calls." = "Audio/Video Anrufe nicht erlauben."; +"Prohibit audio/video calls." = "Audio-/Video-Anrufe nicht erlauben."; /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Unwiederbringliches löschen von Nachrichten nicht erlauben."; @@ -2461,6 +2497,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Erfahren Sie in unserem GitHub-Repository mehr dazu."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Bestätigungen sind deaktiviert"; + /* No comment provided by engineer. */ "received answer…" = "Antwort erhalten…"; @@ -2510,7 +2549,7 @@ "Reject" = "Ablehnen"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Kontakt ablehnen (der Absender wird NICHT benachrichtigt)"; +"Reject (sender NOT notified)" = "Kontakt ablehnen (der Absender wird NICHT benachrichtigt)"; /* No comment provided by engineer. */ "Reject contact request" = "Kontaktanfrage ablehnen"; @@ -2717,7 +2756,7 @@ "Send a live message - it will update for the recipient(s) as you type it" = "Eine Live Nachricht senden - der/die Empfänger sieht/sehen Nachrichtenaktualisierungen, während Sie sie eingeben"; /* No comment provided by engineer. */ -"Send delivery receipts to" = "Zustellungs-Quittierungen versenden an"; +"Send delivery receipts to" = "Empfangsbestätigungen senden an"; /* No comment provided by engineer. */ "Send direct message" = "Direktnachricht senden"; @@ -2741,7 +2780,7 @@ "Send questions and ideas" = "Senden Sie Fragen und Ideen"; /* No comment provided by engineer. */ -"Send receipts" = "Quittierungen versenden"; +"Send receipts" = "Bestätigungen senden"; /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "Senden Sie diese aus dem Fotoalbum oder von individuellen Tastaturen."; @@ -2762,10 +2801,16 @@ "Sending file will be stopped." = "Das Senden der Datei wird beendet."; /* No comment provided by engineer. */ -"Sending receipts is disabled for %lld contacts" = "Das Senden von Empfangsbestätigungen an %lld Kontakte ist deaktiviert"; +"Sending receipts is disabled for %lld contacts" = "Sendebestätigungen sind für %lld Kontakte deaktiviert"; /* No comment provided by engineer. */ -"Sending receipts is enabled for %lld contacts" = "Das Senden von Empfangsbestätigungen an %lld Kontakte ist aktiviert"; +"Sending receipts is disabled for %lld groups" = "Sendebestätigungen sind für %lld Gruppen deaktiviert"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Sendebestätigungen sind für %lld Kontakte aktiviert"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Sendebestätigungen sind für %lld Gruppen aktiviert"; /* No comment provided by engineer. */ "Sending via" = "Senden über"; @@ -2804,7 +2849,7 @@ "Set contact name…" = "Kontaktname festlegen…"; /* No comment provided by engineer. */ -"Set group preferences" = "Gruppenpräferenzen einstellen"; +"Set group preferences" = "Gruppen-Präferenzen einstellen"; /* No comment provided by engineer. */ "Set it instead of system authentication." = "Anstelle der System-Authentifizierung festlegen."; @@ -2851,6 +2896,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Entwickleroptionen anzeigen"; +/* No comment provided by engineer. */ +"Show last messages" = "Letzte Nachrichten anzeigen"; + /* No comment provided by engineer. */ "Show preview" = "Vorschau anzeigen"; @@ -2867,7 +2915,7 @@ "SimpleX Chat security was audited by Trail of Bits." = "Die Sicherheit von SimpleX Chat wurde von Trail of Bits überprüft."; /* simplex link type */ -"SimpleX contact address" = "SimpleX Kontaktadressen-Link"; +"SimpleX contact address" = "SimpleX-Kontaktadressen-Link"; /* notification */ "SimpleX encrypted message or connection event" = "SimpleX verschlüsselte Nachricht oder Verbindungs-Ereignis"; @@ -2879,19 +2927,19 @@ "SimpleX links" = "SimpleX-Links"; /* No comment provided by engineer. */ -"SimpleX Lock" = "SimpleX Sperre"; +"SimpleX Lock" = "SimpleX-Sperre"; /* No comment provided by engineer. */ "SimpleX Lock mode" = "SimpleX Sperr-Modus"; /* No comment provided by engineer. */ -"SimpleX Lock not enabled!" = "SimpleX Sperre ist nicht aktiviert!"; +"SimpleX Lock not enabled!" = "SimpleX-Sperre ist nicht aktiviert!"; /* No comment provided by engineer. */ -"SimpleX Lock turned on" = "SimpleX Sperre aktiviert"; +"SimpleX Lock turned on" = "SimpleX-Sperre aktiviert"; /* simplex link type */ -"SimpleX one-time invitation" = "SimpleX Einmal-Link"; +"SimpleX one-time invitation" = "SimpleX-Einmal-Einladung"; /* No comment provided by engineer. */ "Skip" = "Überspringen"; @@ -2899,6 +2947,9 @@ /* No comment provided by engineer. */ "Skipped messages" = "Übersprungene Nachrichten"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Kleine Gruppen (max. 20)"; + /* No comment provided by engineer. */ "SMP servers" = "SMP-Server"; @@ -3062,7 +3113,7 @@ "The profile is only shared with your contacts." = "Das Profil wird nur mit Ihren Kontakten geteilt."; /* No comment provided by engineer. */ -"The second tick we missed! ✅" = "Das zweite Häkchen, welches wir vermisst haben! ✅"; +"The second tick we missed! ✅" = "Wir haben das zweite Häkchen vermisst! ✅"; /* No comment provided by engineer. */ "The sender will NOT be notified" = "Der Absender wird NICHT benachrichtigt"; @@ -3083,7 +3134,7 @@ "These settings are for your current profile **%@**." = "Diese Einstellungen betreffen Ihr aktuelles Profil **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Diese können in den Kontakteinstellungen überschrieben werden"; +"They can be overridden in contact and group settings." = "Sie können in den Kontakteinstellungen überschrieben werden."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; @@ -3097,6 +3148,9 @@ /* notification title */ "this contact" = "Dieser Kontakt"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %lld Mitglieder hat."; + /* No comment provided by engineer. */ "This group no longer exists." = "Diese Gruppe existiert nicht mehr."; @@ -3109,9 +3163,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Um eine Verbindung herzustellen, kann Ihr Kontakt den QR-Code scannen oder den Link in der App verwenden."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Um das für eine Inkognito-Verbindung verwendete Profil zu finden, tippen Sie oben im Chat auf den Kontakt- oder Gruppennamen."; - /* No comment provided by engineer. */ "To make a new connection" = "Um eine Verbindung mit einem neuen Kontakt zu erstellen"; @@ -3122,7 +3173,7 @@ "To protect timezone, image/voice files use UTC." = "Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen."; /* No comment provided by engineer. */ -"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Um Ihre Informationen zu schützen, schalten Sie die SimpleX Sperre ein.\nSie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird."; +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Um Ihre Informationen zu schützen, schalten Sie die SimpleX-Sperre ein.\nSie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird."; /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können."; @@ -3157,7 +3208,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Die Aufnahme einer Sprachnachricht ist nicht möglich"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Unerwarteter Fehler: %@"; /* No comment provided by engineer. */ @@ -3244,17 +3295,23 @@ /* No comment provided by engineer. */ "Use chat" = "Verwenden Sie Chat"; +/* No comment provided by engineer. */ +"Use current profile" = "Nutzen Sie das aktuelle Profil"; + /* No comment provided by engineer. */ "Use for new connections" = "Für neue Verbindungen nutzen"; /* No comment provided by engineer. */ "Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Nutzen Sie das neue Inkognito-Profil"; + /* No comment provided by engineer. */ "Use server" = "Server nutzen"; /* No comment provided by engineer. */ -"Use SimpleX Chat servers?" = "Verwenden Sie SimpleX Chat Server?"; +"Use SimpleX Chat servers?" = "Verwenden Sie SimpleX-Chat-Server?"; /* No comment provided by engineer. */ "User profile" = "Benutzerprofil"; @@ -3263,7 +3320,7 @@ "Using .onion hosts requires compatible VPN provider." = "Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich."; /* No comment provided by engineer. */ -"Using SimpleX Chat servers." = "Verwende SimpleX Chat Server."; +"Using SimpleX Chat servers." = "Verwendung von SimpleX-Chat-Servern."; /* No comment provided by engineer. */ "v%@ (%@)" = "v%@ (%@)"; @@ -3446,7 +3503,7 @@ "You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten"; /* No comment provided by engineer. */ -"You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX Sperre über die Einstellungen aktivieren."; +"You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX-Sperre über die Einstellungen aktivieren."; /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Um Nachrichteninhalte zu formatieren, können Sie Markdowns verwenden:"; @@ -3479,7 +3536,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert."; /* No comment provided by engineer. */ -"You invited your contact" = "Sie haben Ihren Kontakt eingeladen"; +"You invited a contact" = "Sie haben Ihren Kontakt eingeladen"; /* No comment provided by engineer. */ "You joined this group" = "Sie sind dieser Gruppe beigetreten"; @@ -3559,9 +3616,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Ihr Chat-Profil wird an Gruppenmitglieder gesendet"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Ihr Chat-Profil wird an Ihren Kontakt gesendet"; - /* No comment provided by engineer. */ "Your chat profiles" = "Meine Chat-Profile"; @@ -3596,10 +3650,10 @@ "Your privacy" = "Meine Privatsphäre"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\nSimpleX-Server können Ihr Profil nicht einsehen."; +"Your profile **%@** will be shared." = "Ihr Profil **%@** wird geteilt."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ihr Profil wird auf Ihrem Gerät gespeichert und nur mit Ihren Kontakten geteilt.\nSimpleX-Server können Ihr Profil nicht einsehen."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index ffbce89e21..c4180ea153 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -50,7 +50,7 @@ "[Send us email](mailto:chat@simplex.chat)" = "[Contacta vía email](mailto:chat@simplex.chat)"; /* No comment provided by engineer. */ -"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Estrella en GitHub] (https://github.com/simplex-chat/simplex-chat)"; +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Estrella en GitHub](https://github.com/simplex-chat/simplex-chat)"; /* No comment provided by engineer. */ "**Add new contact**: to create your one-time QR Code for your contact." = "**Añadir nuevo contacto**: para crear tu código QR o enlace de un uso para tu contacto."; @@ -88,6 +88,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*bold*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Historial"; + +/* copied message info */ +"## In reply to" = "## En respuesta a"; + /* No comment provided by engineer. */ "#secret#" = "#secreto#"; @@ -106,6 +115,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ y %@ conectados"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ a las %2$@:"; @@ -124,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "¡ %@ quiere contactar!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ y %lld miembros más conectados"; + /* copied message info */ "%@:" = "%@:"; @@ -245,10 +260,7 @@ "A new contact" = "Contacto nuevo"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Se enviará un perfil aleatorio al contacto del que recibió este enlace"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Se enviará un perfil aleatorio a tu contacto"; +"A new random profile will be shared." = "Se compartirá un perfil nuevo aleatorio."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Se usará una conexión TCP independiente **por cada perfil que tengas en la aplicación**."; @@ -269,7 +281,7 @@ "About SimpleX" = "Acerca de SimpleX"; /* No comment provided by engineer. */ -"About SimpleX address" = "Acerca de dirección SimpleX"; +"About SimpleX address" = "Acerca de la dirección SimpleX"; /* No comment provided by engineer. */ "About SimpleX Chat" = "Sobre SimpleX Chat"; @@ -285,12 +297,12 @@ "Accept" = "Aceptar"; /* No comment provided by engineer. */ -"Accept contact" = "Aceptar contacto"; +"Accept connection request?" = "¿Aceptar solicitud de conexión?"; /* notification body */ "Accept contact request from %@?" = "¿Aceptar solicitud de contacto de %@?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Aceptar incógnito"; /* call status */ @@ -420,10 +432,10 @@ "always" = "siempre"; /* No comment provided by engineer. */ -"Always use relay" = "Siempre usar relay"; +"Always use relay" = "Usar siempre retransmisor"; /* No comment provided by engineer. */ -"An empty chat profile with the provided name is created, and the app opens as usual." = "Se creará un perfil de chat vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre."; +"An empty chat profile with the provided name is created, and the app opens as usual." = "Se creará un perfil vacío con el nombre proporcionado, y la aplicación se abrirá como de costumbre."; /* No comment provided by engineer. */ "Answer call" = "Responder llamada"; @@ -525,7 +537,7 @@ "Both you and your contact can send voice messages." = "Tanto tú como tu contacto podéis enviar mensajes de voz."; /* No comment provided by engineer. */ -"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Mediante perfil de Chat (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Mediante perfil (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; /* No comment provided by engineer. */ "Call already ended!" = "¡La llamada ha terminado!"; @@ -696,11 +708,17 @@ /* server test step */ "Connect" = "Conectar"; +/* No comment provided by engineer. */ +"Connect directly" = "Conectar directamente"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Conectar incognito"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "contacta con los desarrolladores de SimpleX Chat."; /* No comment provided by engineer. */ -"Connect via contact link?" = "¿Conectar mediante enlace de contacto?"; +"Connect via contact link" = "Conectar mediante enlace de contacto"; /* No comment provided by engineer. */ "Connect via group link?" = "¿Conectar mediante enlace de grupo?"; @@ -712,7 +730,7 @@ "Connect via link / QR code" = "Conecta vía enlace / Código QR"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "¿Conectar mediante enlace de un uso?"; +"Connect via one-time link" = "Conectar mediante enlace de un sólo uso"; /* No comment provided by engineer. */ "connected" = "conectado"; @@ -756,9 +774,6 @@ /* chat list item title (it should not be shown */ "connection established" = "conexión establecida"; -/* No comment provided by engineer. */ -"Connection request" = "Solicitud de conexión"; - /* No comment provided by engineer. */ "Connection request sent!" = "¡Solicitud de conexión enviada!"; @@ -817,7 +832,7 @@ "Create" = "Crear"; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Crear una dirección para que otras personas se puedan conectar contigo."; +"Create an address to let people connect with you." = "Crea una dirección para que otras personas puedan conectar contigo."; /* server test step */ "Create file" = "Crear archivo"; @@ -838,10 +853,10 @@ "Create secret group" = "Crea grupo secreto"; /* No comment provided by engineer. */ -"Create SimpleX address" = "Crear dirección SimpleX"; +"Create SimpleX address" = "Crear tu dirección SimpleX"; /* No comment provided by engineer. */ -"Create your profile" = "Crear tu perfil"; +"Create your profile" = "Crea tu perfil"; /* No comment provided by engineer. */ "Created on %@" = "Creado en %@"; @@ -928,7 +943,7 @@ "days" = "días"; /* No comment provided by engineer. */ -"Decentralized" = "Descentralizado"; +"Decentralized" = "Descentralizada"; /* message decrypt error item */ "Decryption error" = "Error descifrado"; @@ -964,10 +979,10 @@ "Delete chat archive?" = "¿Eliminar archivo del chat?"; /* No comment provided by engineer. */ -"Delete chat profile" = "Eliminar perfil de chat"; +"Delete chat profile" = "Eliminar perfil"; /* No comment provided by engineer. */ -"Delete chat profile?" = "¿Eliminar el perfil de chat?"; +"Delete chat profile?" = "¿Eliminar el perfil?"; /* No comment provided by engineer. */ "Delete connection" = "Eliminar conexión"; @@ -991,7 +1006,7 @@ "Delete files and media?" = "Eliminar archivos y multimedia?"; /* No comment provided by engineer. */ -"Delete files for all chat profiles" = "Eliminar los archivos de todos los perfiles"; +"Delete files for all chat profiles" = "Eliminar archivos de todos los perfiles"; /* chat feature */ "Delete for everyone" = "Eliminar para todos"; @@ -1059,6 +1074,9 @@ /* rcv group event chat item */ "deleted group" = "grupo eliminado"; +/* No comment provided by engineer. */ +"Delivery" = "Entrega"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "¡Las confirmaciones de entrega están desactivadas!"; @@ -1099,7 +1117,7 @@ "Direct messages between members are prohibited in this group." = "Los mensajes directos entre miembros del grupo no están permitidos."; /* No comment provided by engineer. */ -"Disable (keep overrides)" = "Desactivar (mantener anulaciones)"; +"Disable (keep overrides)" = "Desactivar (conservando anulaciones)"; /* No comment provided by engineer. */ "Disable for all" = "Desactivar para todos"; @@ -1107,6 +1125,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "Desactivar Bloqueo SimpleX"; +/* No comment provided by engineer. */ +"disabled" = "desactivado"; + /* No comment provided by engineer. */ "Disappearing message" = "Mensaje temporal"; @@ -1141,7 +1162,7 @@ "Do NOT use SimpleX for emergency calls." = "NO uses SimpleX para llamadas de emergencia."; /* No comment provided by engineer. */ -"Don't create address" = "No crear dirección"; +"Don't create address" = "No crear dirección SimpleX"; /* No comment provided by engineer. */ "Don't enable" = "No activar"; @@ -1258,10 +1279,10 @@ "encryption ok for %@" = "cifrado ok para %@"; /* chat item text */ -"encryption re-negotiation allowed" = "renegociación de cifrado permitida"; +"encryption re-negotiation allowed" = "renegociar el cifrado permitido"; /* chat item text */ -"encryption re-negotiation allowed for %@" = "renegociación de cifrado permitida para %@"; +"encryption re-negotiation allowed for %@" = "renegociar el cifrado permitido para %@"; /* chat item text */ "encryption re-negotiation required" = "se requiere renegociar el cifrado"; @@ -1452,6 +1473,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; +/* No comment provided by engineer. */ +"event happened" = "evento ocurrido"; + /* No comment provided by engineer. */ "Exit without saving" = "Salir sin guardar"; @@ -1501,13 +1525,13 @@ "Files and media prohibited!" = "¡Archivos y multimedia no permitidos!"; /* No comment provided by engineer. */ -"Filter unread and favorite chats." = "Filtrar chats no leídos y favoritos."; +"Filter unread and favorite chats." = "Filtra chats no leídos y favoritos."; /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "¡Por fin los tenemos! 🚀"; /* No comment provided by engineer. */ -"Find chats faster" = "Encontrar chats mas rápido"; +"Find chats faster" = "Encuentra chats mas rápido"; /* No comment provided by engineer. */ "Fix" = "Reparar"; @@ -1519,7 +1543,7 @@ "Fix connection?" = "¿Reparar conexión?"; /* No comment provided by engineer. */ -"Fix encryption after restoring backups." = "Reparar el cifrado tras restaurar copias de seguridad."; +"Fix encryption after restoring backups." = "Repara el cifrado tras restaurar copias de seguridad."; /* No comment provided by engineer. */ "Fix not supported by contact" = "Corrección no compatible con el contacto"; @@ -1633,7 +1657,7 @@ "Hidden" = "Oculto"; /* No comment provided by engineer. */ -"Hidden chat profiles" = "Perfiles Chat ocultos"; +"Hidden chat profiles" = "Perfiles ocultos"; /* No comment provided by engineer. */ "Hidden profile password" = "Contraseña del perfil oculto"; @@ -1650,7 +1674,7 @@ /* No comment provided by engineer. */ "Hide:" = "Ocultar:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Historial"; /* time unit */ @@ -1719,7 +1743,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Configuración del servidor mejorada"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "En respuesta a"; /* No comment provided by engineer. */ @@ -1729,10 +1753,7 @@ "Incognito mode" = "Modo incógnito"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "El modo incógnito no se admite aquí, tu perfil principal aparecerá en miembros del grupo"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "La función del modo incógnito es proteger la identidad del perfil principal: por cada contacto nuevo se genera un perfil aleatorio."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto."; /* chat list item description */ "incognito via contact address link" = "en modo incógnito mediante enlace de dirección del contacto"; @@ -1797,6 +1818,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "¡Dirección de servidor no válida!"; +/* item status text */ +"Invalid status" = "Estado no válido"; + /* No comment provided by engineer. */ "Invitation expired!" = "¡Invitación caducada!"; @@ -1813,7 +1837,7 @@ "Invite to group" = "Invitar al grupo"; /* No comment provided by engineer. */ -"invited" = "ha invitado a"; +"invited" = "ha sido invitado"; /* rcv group event chat item */ "invited %@" = "ha invitado a %@"; @@ -1876,7 +1900,7 @@ "Joining group" = "Entrando al grupo"; /* No comment provided by engineer. */ -"Keep your connections" = "Mantén tus conexiones"; +"Keep your connections" = "Conserva tus conexiones"; /* No comment provided by engineer. */ "Keychain error" = "Error Keychain"; @@ -1939,7 +1963,7 @@ "Make one message disappear" = "Escribir un mensaje temporal"; /* No comment provided by engineer. */ -"Make profile private!" = "¡Hacer un perfil privado!"; +"Make profile private!" = "¡Hacer perfil privado!"; /* No comment provided by engineer. */ "Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Asegúrate de que las direcciones del servidor %@ tienen el formato correcto, están separadas por líneas y no duplicadas (%@)."; @@ -1960,7 +1984,7 @@ "Mark verified" = "Marcar como verificado"; /* No comment provided by engineer. */ -"Markdown in messages" = "Sintaxis markdown en los mensajes"; +"Markdown in messages" = "Sintaxis Markdown"; /* marked deleted chat item preview text */ "marked deleted" = "marcado eliminado"; @@ -1986,7 +2010,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Error en la entrega del mensaje"; /* No comment provided by engineer. */ @@ -2058,11 +2082,14 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "¡Pronto habrá más mejoras!"; +/* item status description */ +"Most likely this connection is deleted." = "Probablemente la conexión ha sido eliminada."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Lo más probable es que este contacto haya eliminado la conexión contigo."; /* No comment provided by engineer. */ -"Multiple chat profiles" = "Múltiples perfiles de chat"; +"Multiple chat profiles" = "Múltiples perfiles"; /* No comment provided by engineer. */ "Mute" = "Silenciar"; @@ -2130,6 +2157,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Sin contactos que añadir"; +/* No comment provided by engineer. */ +"No delivery information" = "Sin información de entrega"; + /* No comment provided by engineer. */ "No device token!" = "¡Sin dispositivo token!"; @@ -2213,10 +2243,10 @@ "Only group owners can change group preferences." = "Sólo los propietarios pueden modificar las preferencias de grupo."; /* No comment provided by engineer. */ -"Only group owners can enable files and media." = "Sólo los propietarios pueden activar archivos y multimedia."; +"Only group owners can enable files and media." = "Sólo los propietarios del grupo pueden activar los archivos y multimedia."; /* No comment provided by engineer. */ -"Only group owners can enable voice messages." = "Sólo los propietarios pueden activar los mensajes de voz."; +"Only group owners can enable voice messages." = "Sólo los propietarios del grupo pueden activar los mensajes de voz."; /* No comment provided by engineer. */ "Only you can add message reactions." = "Sólo tú puedes añadir reacciones a los mensajes."; @@ -2302,8 +2332,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Pegar enlace recibido"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Pega el enlace que has recibido en el recuadro para conectar con tu contacto."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Pega el enlace que has recibido en el recuadro para conectar con tu contacto."; /* No comment provided by engineer. */ "peer-to-peer" = "p2p"; @@ -2399,34 +2429,34 @@ "Profile update will be sent to your contacts." = "La actualización del perfil se enviará a tus contactos."; /* No comment provided by engineer. */ -"Prohibit audio/video calls." = "Prohibir las llamadas y videollamadas."; +"Prohibit audio/video calls." = "No se permiten llamadas y videollamadas."; /* No comment provided by engineer. */ -"Prohibit irreversible message deletion." = "Prohibir la eliminación irreversible de mensajes."; +"Prohibit irreversible message deletion." = "No se permite la eliminación irreversible de mensajes."; /* No comment provided by engineer. */ -"Prohibit message reactions." = "Prohibir reacciones a mensajes."; +"Prohibit message reactions." = "No se permiten reacciones a los mensajes."; /* No comment provided by engineer. */ -"Prohibit messages reactions." = "Prohibir reacciones a mensajes."; +"Prohibit messages reactions." = "No se permiten reacciones a los mensajes."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Prohibir mensajes directos a miembros."; +"Prohibit sending direct messages to members." = "No se permiten mensajes directos entre miembros."; /* No comment provided by engineer. */ -"Prohibit sending disappearing messages." = "Prohibir envío de mensajes temporales."; +"Prohibit sending disappearing messages." = "No se permiten mensajes temporales."; /* No comment provided by engineer. */ "Prohibit sending files and media." = "No permitir el envío de archivos y multimedia."; /* No comment provided by engineer. */ -"Prohibit sending voice messages." = "Prohibir el envío de mensajes de voz."; +"Prohibit sending voice messages." = "No se permiten mensajes de voz."; /* No comment provided by engineer. */ "Protect app screen" = "Proteger la pantalla de la aplicación"; /* No comment provided by engineer. */ -"Protect your chat profiles with a password!" = "¡Protege tus perfiles de chat con contraseña!"; +"Protect your chat profiles with a password!" = "¡Protege tus perfiles con contraseña!"; /* No comment provided by engineer. */ "Protocol timeout" = "Tiempo de espera del protocolo"; @@ -2435,7 +2465,7 @@ "Protocol timeout per KB" = "Límite de espera del protocolo por KB"; /* No comment provided by engineer. */ -"Push notifications" = "Notificaciones push"; +"Push notifications" = "Notificaciones automáticas"; /* No comment provided by engineer. */ "Rate the app" = "Valora la aplicación"; @@ -2461,6 +2491,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Más información en nuestro repositorio GitHub."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Las confirmaciones están desactivadas"; + /* No comment provided by engineer. */ "received answer…" = "respuesta recibida…"; @@ -2480,7 +2513,7 @@ "Received message" = "Mensaje entrante"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción se cambiará. El cambio se completará cuando el remitente esté en línea."; +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Se detendrá la recepción del archivo."; @@ -2489,7 +2522,7 @@ "Receiving via" = "Recibiendo vía"; /* No comment provided by engineer. */ -"Recipients see updates as you type them." = "Los destinatarios ven actualizaciones mientras les escribes."; +"Recipients see updates as you type them." = "Los destinatarios ven actualizarse mientras escribes."; /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconectar todos los servidores conectados para forzar la entrega del mensaje. Se usa tráfico adicional."; @@ -2498,19 +2531,19 @@ "Reconnect servers?" = "¿Reconectar servidores?"; /* No comment provided by engineer. */ -"Record updated at" = "Registro actualizado a las"; +"Record updated at" = "Registro actualiz."; /* copied message info */ -"Record updated at: %@" = "Registro actualizado a las: %@"; +"Record updated at: %@" = "Registro actualiz: %@"; /* No comment provided by engineer. */ -"Reduced battery usage" = "Uso de la batería reducido"; +"Reduced battery usage" = "Reducción del uso de batería"; /* reject incoming call via notification */ "Reject" = "Rechazar"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Rechazar contacto (NO se notifica al remitente)"; +"Reject (sender NOT notified)" = "Rechazar contacto (NO se notifica al remitente)"; /* No comment provided by engineer. */ "Reject contact request" = "Rechazar solicitud de contacto"; @@ -2519,10 +2552,10 @@ "rejected call" = "llamada rechazada"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "El servidor de retransmisión sólo se usará en caso necesario. Un tercero podría ver tu dirección IP."; +"Relay server is only used if necessary. Another party can observe your IP address." = "El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP."; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "El servidor de retransmisión protege tu dirección IP, pero puede observar la duración de la llamada."; +"Relay server protects your IP address, but it can observe the duration of the call." = "El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada."; /* No comment provided by engineer. */ "Remove" = "Eliminar"; @@ -2534,7 +2567,7 @@ "Remove member?" = "¿Expulsar miembro?"; /* No comment provided by engineer. */ -"Remove passphrase from keychain?" = "¿Eliminar la contraseña del llavero?"; +"Remove passphrase from keychain?" = "¿Eliminar contraseña de Keychain?"; /* No comment provided by engineer. */ "removed" = "expulsado"; @@ -2627,7 +2660,7 @@ "Save archive" = "Guardar archivo"; /* No comment provided by engineer. */ -"Save auto-accept settings" = "Guardar configuración de aceptación automática (auto-accept)"; +"Save auto-accept settings" = "Guardar configuración de auto aceptar"; /* No comment provided by engineer. */ "Save group profile" = "Guardar perfil de grupo"; @@ -2764,9 +2797,15 @@ /* No comment provided by engineer. */ "Sending receipts is disabled for %lld contacts" = "El envío de confirmaciones está desactivado para %lld contactos"; +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "El envío de confirmaciones está desactivado para %lld grupos"; + /* No comment provided by engineer. */ "Sending receipts is enabled for %lld contacts" = "El envío de confirmaciones está activado para %lld contactos"; +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "El envío de confirmaciones está activado para %lld grupos"; + /* No comment provided by engineer. */ "Sending via" = "Enviando vía"; @@ -2851,6 +2890,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Mostrar opciones de desarrollador"; +/* No comment provided by engineer. */ +"Show last messages" = "Mostrar último mensaje"; + /* No comment provided by engineer. */ "Show preview" = "Mostrar vista previa"; @@ -2899,6 +2941,9 @@ /* No comment provided by engineer. */ "Skipped messages" = "Mensajes omitidos"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Grupos pequeños (máx. 20)"; + /* No comment provided by engineer. */ "SMP servers" = "Servidores SMP"; @@ -2924,7 +2969,7 @@ "Stop" = "Detener"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, previamente debes detener Chat"; +"Stop chat to enable database actions" = "Detén SimpleX para habilitar las acciones sobre la base de datos"; /* No comment provided by engineer. */ "Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes."; @@ -3053,7 +3098,7 @@ "The message will be marked as moderated for all members." = "El mensaje será marcado como moderado para todos los miembros."; /* No comment provided by engineer. */ -"The next generation of private messaging" = "La próxima generación de mensajería privada"; +"The next generation of private messaging" = "La nueva generación de mensajería privada"; /* No comment provided by engineer. */ "The old database was not removed during the migration, it can be deleted." = "La base de datos antigua no se eliminó durante la migración, puede eliminarse."; @@ -3074,16 +3119,16 @@ "Theme" = "Tema"; /* No comment provided by engineer. */ -"There should be at least one user profile." = "Debe haber al menos un perfil de usuario."; +"There should be at least one user profile." = "Debe haber al menos un perfil."; /* No comment provided by engineer. */ -"There should be at least one visible user profile." = "Debe haber al menos un perfil de usuario visible."; +"There should be at least one visible user profile." = "Debe haber al menos un perfil visible."; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Esta configuración afecta a tu perfil actual **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Se pueden anular en la configuración de contactos"; +"They can be overridden in contact and group settings." = "Se pueden anular en la configuración de contactos."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán."; @@ -3097,6 +3142,9 @@ /* notification title */ "this contact" = "este contacto"; +/* 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."; + /* No comment provided by engineer. */ "This group no longer exists." = "Este grupo ya no existe."; @@ -3109,9 +3157,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Para conocer el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat."; - /* No comment provided by engineer. */ "To make a new connection" = "Para hacer una conexión nueva"; @@ -3128,7 +3173,7 @@ "To record voice message please grant permission to use Microphone." = "Para grabar el mensaje de voz concede permiso para usar el micrófono."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página **Mis perfiles**."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Para hacer visible tu perfil oculto, introduce la contraseña en el campo de búsqueda del menú **Mis perfiles**."; /* No comment provided by engineer. */ "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."; @@ -3157,7 +3202,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "No se puede grabar mensaje de voz"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Error inesperado: %@"; /* No comment provided by engineer. */ @@ -3170,7 +3215,7 @@ "Unhide" = "Mostrar"; /* No comment provided by engineer. */ -"Unhide chat profile" = "Mostrar perfil de chat"; +"Unhide chat profile" = "Mostrar perfil oculto"; /* No comment provided by engineer. */ "Unhide profile" = "Mostrar perfil"; @@ -3244,12 +3289,18 @@ /* No comment provided by engineer. */ "Use chat" = "Usar Chat"; +/* No comment provided by engineer. */ +"Use current profile" = "Usar perfil actual"; + /* No comment provided by engineer. */ "Use for new connections" = "Usar para conexiones nuevas"; /* No comment provided by engineer. */ "Use iOS call interface" = "Usar interfaz de llamada de iOS"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Usar nuevo perfil incógnito"; + /* No comment provided by engineer. */ "Use server" = "Usar servidor"; @@ -3287,7 +3338,7 @@ "via one-time link" = "mediante enlace de un uso"; /* No comment provided by engineer. */ -"via relay" = "mediante servidor relay"; +"via relay" = "mediante retransmisor"; /* No comment provided by engineer. */ "Video call" = "Videollamada"; @@ -3392,7 +3443,7 @@ "You allow" = "Permites"; /* No comment provided by engineer. */ -"You already have a chat profile with the same display name. Please choose another name." = "Tienes un perfil de chat con el mismo nombre mostrado. Debes elegir otro nombre."; +"You already have a chat profile with the same display name. Please choose another name." = "Ya tienes un perfil con este nombre mostrado. Por favor, elige otro nombre."; /* No comment provided by engineer. */ "You are already connected to %@." = "Ya estás conectado a %@."; @@ -3416,7 +3467,7 @@ "You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "También puedes conectarte haciendo clic en el enlace. Si se abre en el navegador, haz clic en el botón **Abrir en aplicación móvil**."; /* No comment provided by engineer. */ -"You can create it later" = "Puedes crearlo más tarde"; +"You can create it later" = "Puedes crearla más tarde"; /* No comment provided by engineer. */ "You can enable later via Settings" = "Puedes activar más tarde en Configuración"; @@ -3425,7 +3476,7 @@ "You can enable them later via app Privacy & Security settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad."; /* No comment provided by engineer. */ -"You can hide or mute a user profile - swipe it to the right." = "Puedes ocultar o silenciar un perfil de usuario: deslízalo hacia la derecha."; +"You can hide or mute a user profile - swipe it to the right." = "Puedes ocultar o silenciar un perfil deslizándolo a la derecha."; /* notification body */ "You can now send messages to %@" = "Ya puedes enviar mensajes a %@"; @@ -3479,7 +3530,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación."; /* No comment provided by engineer. */ -"You invited your contact" = "Has invitado a tu contacto"; +"You invited a contact" = "Has invitado a tu contacto"; /* No comment provided by engineer. */ "You joined this group" = "Te has unido a este grupo"; @@ -3515,7 +3566,7 @@ "You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde."; /* No comment provided by engineer. */ -"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando se acepte tu solicitud de conexión, por favor espere o compruébalo más tarde."; +"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde."; /* No comment provided by engineer. */ "You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde."; @@ -3551,16 +3602,13 @@ "Your calls" = "Llamadas"; /* No comment provided by engineer. */ -"Your chat database" = "Base de datos Chat"; +"Your chat database" = "Base de datos"; /* No comment provided by engineer. */ "Your chat database is not encrypted - set passphrase to encrypt it." = "La base de datos no está cifrada - establece una contraseña para cifrarla."; /* No comment provided by engineer. */ -"Your chat profile will be sent to group members" = "Tu perfil Chat será enviado a los miembros del grupo"; - -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Tu perfil Chat será enviado a tu contacto"; +"Your chat profile will be sent to group members" = "Tu perfil será enviado a los miembros del grupo"; /* No comment provided by engineer. */ "Your chat profiles" = "Mis perfiles"; @@ -3595,14 +3643,14 @@ /* No comment provided by engineer. */ "Your privacy" = "Privacidad"; +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Tu perfil **%@** será compartido."; + /* No comment provided by engineer. */ "Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.\nLos servidores de SimpleX no pueden ver tu perfil."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Tu perfil se enviará al contacto del que has recibido este enlace"; - -/* No comment provided by engineer. */ -"Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo."; +"Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo."; /* No comment provided by engineer. */ "Your random profile" = "Tu perfil aleatorio"; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..a917c4a0b4 --- /dev/null +++ b/apps/ios/fi.lproj/Localizable.strings @@ -0,0 +1,3681 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" (" = " ("; + +/* No comment provided by engineer. */ +" (can be copied)" = " (voidaan kopioida)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_italic_"; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- vakaampi viestien toimitus.\n- hieman paremmat ryhmät.\n- ja paljon muuta!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- ääniviestit enintään 5 minuuttia.\n- mukautettu katoamisaika.\n- historian muokkaaminen."; + +/* No comment provided by engineer. */ +", " = ", "; + +/* No comment provided by engineer. */ +": " = ": "; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 värillinen!"; + +/* No comment provided by engineer. */ +"." = "."; + +/* No comment provided by engineer. */ +"(" = "("; + +/* No comment provided by engineer. */ +")" = ")"; + +/* No comment provided by engineer. */ +"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Osallistu](https://github.com/simplex-chat/simplex-chat#contribute)"; + +/* No comment provided by engineer. */ +"[Send us email](mailto:chat@simplex.chat)" = "[Lähetä meille sähköpostia](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Tähti GitHubissa](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Lisää uusi kontakti**: luo kertakäyttöinen QR-koodi tai linkki kontaktille."; + +/* No comment provided by engineer. */ +"**Create link / QR code** for your contact to use." = "**Luo linkki / QR-koodi* kontaktille."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**e2e-salattu** äänipuhelu"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**e2e-salattu** videopuhelu"; + +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Yksityisempi**: tarkista uudet viestit 20 minuutin välein. Laitetunnus jaetaan SimpleX Chat -palvelimen kanssa, mutta ei sitä, kuinka monta yhteystietoa tai viestiä sinulla on."; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Yksityisin**: älä käytä SimpleX Chat -ilmoituspalvelinta, tarkista viestit ajoittain taustalla (riippuu siitä, kuinka usein käytät sovellusta)."; + +/* No comment provided by engineer. */ +"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Liitä vastaanotettu linkki** tai avaa se selaimessa ja napauta **Avaa mobiilisovelluksessa**."; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Huomaa**: et voi palauttaa tai muuttaa tunnuslausetta, jos kadotat sen."; + +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Suositus**: laitetunnus ja ilmoitukset lähetetään SimpleX Chat -ilmoituspalvelimelle, mutta ei viestin sisältöä, kokoa tai sitä, keneltä se on peräisin."; + +/* No comment provided by engineer. */ +"**Scan QR code**: to connect to your contact in person or via video call." = "**Skannaa QR-koodi**: muodosta yhteys kontaktiisi henkilökohtaisesti tai videopuhelun kautta."; + +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Varoitus**: Välittömät push-ilmoitukset vaativat tunnuslauseen, joka on tallennettu Keychainiin."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*bold*"; + +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Historia"; + +/* copied message info */ +"## In reply to" = "## vastauksena"; + +/* No comment provided by engineer. */ +"#secret#" = "#salaisuus#"; + +/* No comment provided by engineer. */ +"%@" = "% @"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (nykyinen)"; + +/* copied message info */ +"%@ (current):" = "% (nykyinen):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / % @"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ % @"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ ja %@ yhdistetty"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ klo %2$@:"; + +/* notification title */ +"%@ is connected!" = "%@ on yhdistetty!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ ei ole vahvistettu"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ on vahvistettu"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ palvelimet"; + +/* notification title */ +"%@ wants to connect!" = "%@ haluaa muodostaa yhteyden!"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ ja %lld muut jäsenet yhdistetty"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d päivää"; + +/* time interval */ +"%d hours" = "%d tuntia"; + +/* time interval */ +"%d min" = "%d min"; + +/* time interval */ +"%d months" = "%d kuukautta"; + +/* time interval */ +"%d sec" = "%d sek"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d ohitettua viestiä"; + +/* time interval */ +"%d weeks" = "%d viikkoa"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld kontaktia valittu"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld tiedosto(a), joiden kokonaiskoko on %@"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld jäsenet"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld minuuttia"; + +/* No comment provided by engineer. */ +"%lld second(s)" = "%lld sekunti(a)"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld sekuntia"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldd"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldh"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldm"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldmth"; + +/* No comment provided by engineer. */ +"%llds" = "%llds"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldw"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "%u viestien salauksen purku epäonnistui."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u viestit ohitettu."; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p> Hei! </p>\n<p> <a href=\"%@\"> Ollaan yhteydessä SimpleX Chatin kautta</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~strike~"; + +/* No comment provided by engineer. */ +"0s" = "0s"; + +/* time interval */ +"1 day" = "1 päivä"; + +/* time interval */ +"1 hour" = "1 tunti"; + +/* No comment provided by engineer. */ +"1 minute" = "1 minuutti"; + +/* time interval */ +"1 month" = "1 kuukausi"; + +/* time interval */ +"1 week" = "1 viikko"; + +/* No comment provided by engineer. */ +"1-time link" = "Kertakäyttölinkki"; + +/* No comment provided by engineer. */ +"5 minutes" = "5 minuuttia"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 sekuntia"; + +/* No comment provided by engineer. */ +"A few more things" = "Muutama asia lisää"; + +/* notification title */ +"A new contact" = "Uusi kontakti"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Uusi satunnainen profiili jaetaan."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Erillistä TCP-yhteyttä käytetään **jokaiselle sovelluksessa olevalle chat-profiilille**."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Jokaiselle kontaktille ja ryhmän jäsenelle käytetään erillistä TCP-yhteyttä**.\n**Huomaa**: jos kontakteja on useita, akun ja liikenteen kulutus voi olla huomattavasti suurempi ja jotkin yhteydet voivat epäonnistua."; + +/* No comment provided by engineer. */ +"Abort" = "Keskeytä"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Keskeytä osoitteenvaihto"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Keskeytä osoitteenvaihto?"; + +/* No comment provided by engineer. */ +"About SimpleX" = "Tietoja SimpleX:stä"; + +/* No comment provided by engineer. */ +"About SimpleX address" = "Tietoja SimpleX osoitteesta"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "Tietoja SimpleX Chatistä"; + +/* No comment provided by engineer. */ +"above, then choose:" = "edellä, valitse sitten:"; + +/* No comment provided by engineer. */ +"Accent color" = "Korostusväri"; + +/* accept contact request via notification + accept incoming call via notification */ +"Accept" = "Hyväksy"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Hyväksy yhteyspyyntö?"; + +/* notification body */ +"Accept contact request from %@?" = "Hyväksy kontaktipyyntö %@:ltä?"; + +/* accept contact request via notification */ +"Accept incognito" = "Hyväksy tuntematon"; + +/* call status */ +"accepted call" = "hyväksytty puhelu"; + +/* 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." = "Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi."; + +/* No comment provided by engineer. */ +"Add preset servers" = "Lisää esiasetettuja palvelimia"; + +/* No comment provided by engineer. */ +"Add profile" = "Lisää profiili"; + +/* No comment provided by engineer. */ +"Add server…" = "Lisää palvelin…"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Lisää palvelimia skannaamalla QR-koodeja."; + +/* No comment provided by engineer. */ +"Add to another device" = "Lisää toiseen laitteeseen"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Lisää tervetuloviesti"; + +/* No comment provided by engineer. */ +"Address" = "Osoite"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta."; + +/* member role */ +"admin" = "ylläpitäjä"; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Ylläpitäjät voivat luoda linkkejä ryhmiin liittymiseen."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Verkon lisäasetukset"; + +/* chat item text */ +"agreeing encryption for %@…" = "salauksesta sovitaan %@:lle…"; + +/* chat item text */ +"agreeing encryption…" = "hyväksyy salausta…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Kaikki sovelluksen tiedot poistetaan."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Kaikki keskustelut ja viestit poistetaan - tätä ei voi kumota!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "Kaikki tiedot poistetaan, kun se syötetään."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Kaikki ryhmän jäsenet pysyvät yhteydessä."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Kaikki viestit poistetaan - tätä ei voi kumota! Viestit poistuvat VAIN sinulta."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Kaikki kontaktisi pysyvät yhteydessä."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään kontakteillesi."; + +/* No comment provided by engineer. */ +"Allow" = "Salli"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Salli puhelut vain, jos kontaktisi sallii ne."; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you." = "Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle."; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "Salli reaktiot viesteihin vain, jos kontaktisi sallii ne."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Salli viestireaktiot."; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "Salli yksityisviestien lähettäminen jäsenille."; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "Salli katoavien viestien lähettäminen."; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages." = "Salli lähetettyjen viestien peruuttamaton poistaminen."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "Salli tiedostojen ja median lähettäminen."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "Salli ääniviestien lähettäminen."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "Salli ääniviestit vain, jos kontaktisi sallii ne."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Salli ääniviestit?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "Salli kontaktiesi lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "Salli kontaktiesi soittaa sinulle."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages." = "Salli kontaktiesi poistaa lähetetyt viestit peruuttamattomasti."; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Salli kontaktiesi lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Salli kontaktiesi lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"Already connected?" = "Oletko jo muodostanut yhteyden?"; + +/* pref value */ +"always" = "aina"; + +/* No comment provided by engineer. */ +"Always use relay" = "Käytä aina relettä"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "Luodaan tyhjä chat-profiili annetulla nimellä, ja sovellus avautuu normaalisti."; + +/* No comment provided by engineer. */ +"Answer call" = "Vastaa puheluun"; + +/* No comment provided by engineer. */ +"App build: %@" = "Sovellusversio: %@"; + +/* No comment provided by engineer. */ +"App icon" = "Sovelluksen kuvake"; + +/* No comment provided by engineer. */ +"App passcode" = "Sovelluksen pääsykoodi"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Sovelluksen pääsykoodi korvataan itsetuhoutuvalla pääsykoodilla."; + +/* No comment provided by engineer. */ +"App version" = "Sovellusversio"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Sovellusversio: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Ulkonäkö"; + +/* No comment provided by engineer. */ +"Attach" = "Liitä"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Ääni- ja videopuhelut"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Ääni- ja videopuhelut"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "äänipuhelu (ei e2e-salattu)"; + +/* chat feature */ +"Audio/video calls" = "Ääni/videopuhelut"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "Ääni-/videopuhelut ovat kiellettyjä."; + +/* PIN entry */ +"Authentication cancelled" = "Tunnistautuminen peruutettu"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Tunnistautuminen epäonnistui"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "Tunnistautuminen vaaditaan ennen kuin puhelu yhdistetään, mutta puheluita voi jäädä vastaamatta."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Tunnistautuminen ei ole käytettävissä"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Hyväksy automaattisesti"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "Hyväksy yhteydenottopyynnöt automaattisesti"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Hyväksy kuvat automaattisesti"; + +/* No comment provided by engineer. */ +"Back" = "Takaisin"; + +/* integrity error chat item */ +"bad message hash" = "virheellinen viestin tarkiste"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Virheellinen viestin tarkiste"; + +/* integrity error chat item */ +"bad message ID" = "virheellinen viestin tunniste"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Virheellinen viestin tunniste"; + +/* No comment provided by engineer. */ +"Better messages" = "Parempia viestejä"; + +/* No comment provided by engineer. */ +"bold" = "lihavoitu"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "Sekä sinä että kontaktisi voivat käyttää viestireaktioita."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages." = "Sekä sinä että kontaktisi voitte peruuttamattomasti poistaa lähetetyt viestit."; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "Sekä sinä että kontaktisi voitte soittaa puheluita."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "Sekä sinä että kontaktisi voitte lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "Sekä sinä että kontaktisi voitte lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Chat-profiilin mukaan (oletus) tai [yhteyden mukaan](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; + +/* No comment provided by engineer. */ +"Call already ended!" = "Puhelu on jo päättynyt!"; + +/* call status */ +"call error" = "soittovirhe"; + +/* call status */ +"call in progress" = "puhelu käynnissä"; + +/* call status */ +"calling…" = "soittaa…"; + +/* No comment provided by engineer. */ +"Calls" = "Puhelut"; + +/* No comment provided by engineer. */ +"Can't delete user profile!" = "Käyttäjäprofiilia ei voi poistaa!"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Kontaktia ei voi kutsua!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Kontakteja ei voi kutsua!"; + +/* No comment provided by engineer. */ +"Cancel" = "Peruuta"; + +/* feature offered item */ +"cancelled %@" = "peruutettu %@"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi"; + +/* No comment provided by engineer. */ +"Cannot receive file" = "Tiedostoa ei voi vastaanottaa"; + +/* No comment provided by engineer. */ +"Change" = "Muuta"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Muutetaanko tietokannan tunnuslause?"; + +/* authentication reason */ +"Change lock mode" = "Vaihda lukitustilaa"; + +/* No comment provided by engineer. */ +"Change member role?" = "Vaihda jäsenroolia?"; + +/* authentication reason */ +"Change passcode" = "Vaihda pääsykoodi"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Vaihda vastaanotto-osoitetta"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Vaihda vastaanotto-osoite?"; + +/* No comment provided by engineer. */ +"Change role" = "Vaihda rooli"; + +/* authentication reason */ +"Change self-destruct mode" = "Vaihda itsetuhotilaa"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "Vaihda itsetuhoutuva pääsykoodi"; + +/* chat item text */ +"changed address for you" = "muuttunut osoite sinulle"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "%1$@:n roolin muuttui %2$@:ksi"; + +/* rcv group event chat item */ +"changed your role to %@" = "roolisi muuttui %@:ksi"; + +/* chat item text */ +"changing address for %@…" = "osoitteen muuttaminen %@:lle…"; + +/* chat item text */ +"changing address…" = "muuttamassa osoitetta…"; + +/* No comment provided by engineer. */ +"Chat archive" = "Chat-arkisto"; + +/* No comment provided by engineer. */ +"Chat console" = "Chat-konsoli"; + +/* No comment provided by engineer. */ +"Chat database" = "Chat-tietokanta"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Chat-tietokanta poistettu"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Chat-tietokanta tuotu"; + +/* No comment provided by engineer. */ +"Chat is running" = "Chat on käynnissä"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Chat on pysäytetty"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Chat-asetukset"; + +/* No comment provided by engineer. */ +"Chats" = "Keskustelut"; + +/* No comment provided by engineer. */ +"Check server address and try again." = "Tarkista palvelimen osoite ja yritä uudelleen."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Kiinalainen ja espanjalainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"Choose file" = "Valitse tiedosto"; + +/* No comment provided by engineer. */ +"Choose from library" = "Valitse kirjastosta"; + +/* No comment provided by engineer. */ +"Clear" = "Tyhjennä"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Tyhjennä keskustelu"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Tyhjennä keskustelu?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Tyhjennä vahvistus"; + +/* No comment provided by engineer. */ +"colored" = "värillinen"; + +/* No comment provided by engineer. */ +"Colors" = "Värit"; + +/* server test step */ +"Compare file" = "Vertaa tiedostoa"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Vertaa turvakoodeja kontaktiesi kanssa."; + +/* No comment provided by engineer. */ +"complete" = "valmis"; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "Määritä ICE-palvelimet"; + +/* No comment provided by engineer. */ +"Confirm" = "Vahvista"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Vahvista tietokannan päivitykset"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Vahvista uusi tunnuslause…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Vahvista pääsykoodi"; + +/* No comment provided by engineer. */ +"Confirm password" = "Vahvista salasana"; + +/* server test step */ +"Connect" = "Yhdistä"; + +/* No comment provided by engineer. */ +"Connect directly" = "Yhdistä suoraan"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Yhdistä Incognito"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "ole yhteydessä SimpleX Chat -kehittäjiin."; + +/* No comment provided by engineer. */ +"Connect via contact link" = "Yhdistä kontaktilinkillä"; + +/* No comment provided by engineer. */ +"Connect via group link?" = "Yhdistetäänkö ryhmälinkin kautta?"; + +/* No comment provided by engineer. */ +"Connect via link" = "Yhdistä linkin kautta"; + +/* No comment provided by engineer. */ +"Connect via link / QR code" = "Yhdistä linkillä / QR-koodilla"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Yhdistä kertalinkillä"; + +/* No comment provided by engineer. */ +"connected" = "yhdistetty"; + +/* No comment provided by engineer. */ +"connecting" = "yhdistää"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "yhdistäminen (hyväksytty)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "yhdistäminen (ilmoitettu)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "yhdistäminen (esitelty)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "yhdistäminen (esittelykutsu)"; + +/* call status */ +"connecting call" = "yhdistää puhelun…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Yhteyden muodostaminen palvelimeen…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Yhteyden muodostaminen palvelimeen... (virhe: %@)"; + +/* chat list item title */ +"connecting…" = "yhdistää…"; + +/* No comment provided by engineer. */ +"Connection" = "Yhteys"; + +/* No comment provided by engineer. */ +"Connection error" = "Yhteysvirhe"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "Yhteysvirhe (AUTH)"; + +/* chat list item title (it should not be shown */ +"connection established" = "yhteys luotu"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Yhteyspyyntö lähetetty!"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Yhteyden aikakatkaisu"; + +/* connection information */ +"connection:%@" = "yhteys:%@"; + +/* No comment provided by engineer. */ +"Contact allows" = "Kontakti sallii"; + +/* No comment provided by engineer. */ +"Contact already exists" = "Kontakti on jo olemassa"; + +/* No comment provided by engineer. */ +"Contact and all messages will be deleted - this cannot be undone!" = "Kontakti ja kaikki viestit poistetaan - tätä ei voi perua!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "kontaktilla on e2e-salaus"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "kontaktilla ei ole e2e-salausta"; + +/* notification */ +"Contact hidden:" = "Kontakti piilotettu:"; + +/* notification */ +"Contact is connected" = "Kontakti on yhdistetty"; + +/* No comment provided by engineer. */ +"Contact is not connected yet!" = "Kontaktia ei ole vielä yhdistetty!"; + +/* No comment provided by engineer. */ +"Contact name" = "Kontaktin nimi"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Kontaktin asetukset"; + +/* No comment provided by engineer. */ +"Contacts" = "Kontaktit"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "Kontaktit voivat merkitä viestit poistettaviksi; voit katsella niitä."; + +/* No comment provided by engineer. */ +"Continue" = "Jatka"; + +/* chat item action */ +"Copy" = "Kopioi"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Ydinversio: v%@"; + +/* No comment provided by engineer. */ +"Create" = "Luo"; + +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "Luo osoite, jolla ihmiset voivat ottaa sinuun yhteyttä."; + +/* server test step */ +"Create file" = "Luo tiedosto"; + +/* No comment provided by engineer. */ +"Create group link" = "Luo ryhmälinkki"; + +/* No comment provided by engineer. */ +"Create link" = "Luo linkki"; + +/* No comment provided by engineer. */ +"Create one-time invitation link" = "Luo kertakutsulinkki"; + +/* server test step */ +"Create queue" = "Luo jono"; + +/* No comment provided by engineer. */ +"Create secret group" = "Luo salainen ryhmä"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "Luo SimpleX-osoite"; + +/* No comment provided by engineer. */ +"Create your profile" = "Luo profiilisi"; + +/* No comment provided by engineer. */ +"Created on %@" = "Luotu %@"; + +/* No comment provided by engineer. */ +"creator" = "luoja"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Nykyinen pääsykoodi"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Nykyinen tunnuslause…"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Nykyinen tuettu enimmäistiedostokoko on %@."; + +/* dropdown time picker choice */ +"custom" = "mukautettu"; + +/* No comment provided by engineer. */ +"Custom time" = "Mukautettu aika"; + +/* No comment provided by engineer. */ +"Dark" = "Tumma"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Tietokannan alentaminen"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Tietokanta salattu!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Tietokannan salaustunnuslause päivitetään ja tallennetaan avainnippuun.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Tietokannan salauksen tunnuslause päivitetään.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Tietokantavirhe"; + +/* No comment provided by engineer. */ +"Database ID" = "Tietokannan tunnus"; + +/* copied message info */ +"Database ID: %d" = "Tietokannan tunnus: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Tietokantatunnukset ja kuljetuseristysvaihtoehto."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "Tietokanta on salattu satunnaisella tunnuslauseella, voit muuttaa sitä."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "Tietokanta on salattu satunnaisella tunnuslauseella. Vaihda se ennen vientiä."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Tietokannan tunnuslause"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Tietokannan tunnuslause ja vienti"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Tietokannan tunnuslause eroaa avainnippuun tallennetusta."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Keskustelun avaamiseen tarvitaan tietokannan tunnuslause."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Tietokannan päivitys"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "tietokantaversio on uudempi kuin sovellus, mutta ei alaspäin siirtymistä varten: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "Tietokanta salataan ja tunnuslause tallennetaan avainnippuun.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "Tietokanta salataan.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "Tietokanta siirretään, kun sovellus käynnistyy uudelleen"; + +/* time unit */ +"days" = "päivää"; + +/* No comment provided by engineer. */ +"Decentralized" = "Hajautettu"; + +/* message decrypt error item */ +"Decryption error" = "Salauksen purkuvirhe"; + +/* pref value */ +"default (%@)" = "oletusarvo (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "oletusarvo (ei)"; + +/* No comment provided by engineer. */ +"default (yes)" = "oletusarvo (kyllä)"; + +/* chat item action */ +"Delete" = "Poista"; + +/* No comment provided by engineer. */ +"Delete address" = "Poista osoite"; + +/* No comment provided by engineer. */ +"Delete address?" = "Poista osoite?"; + +/* No comment provided by engineer. */ +"Delete after" = "Poista jälkeen"; + +/* No comment provided by engineer. */ +"Delete all files" = "Poista kaikki tiedostot"; + +/* No comment provided by engineer. */ +"Delete archive" = "Poista arkisto"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "Poista keskusteluarkisto?"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Poista keskusteluprofiili"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Poista keskusteluprofiili?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Poista yhteys"; + +/* No comment provided by engineer. */ +"Delete contact" = "Poista kontakti"; + +/* No comment provided by engineer. */ +"Delete Contact" = "Poista kontakti"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Poista kontakti?"; + +/* No comment provided by engineer. */ +"Delete database" = "Poista tietokanta"; + +/* server test step */ +"Delete file" = "Poista tiedosto"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Poista tiedostot ja media?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Poista tiedostot kaikista keskusteluprofiileista"; + +/* chat feature */ +"Delete for everyone" = "Poista kaikilta"; + +/* No comment provided by engineer. */ +"Delete for me" = "Poista minulta"; + +/* No comment provided by engineer. */ +"Delete group" = "Poista ryhmä"; + +/* No comment provided by engineer. */ +"Delete group?" = "Poista ryhmä?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Poista kutsu"; + +/* No comment provided by engineer. */ +"Delete link" = "Poista linkki"; + +/* No comment provided by engineer. */ +"Delete link?" = "Poista linkki?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Poista jäsenviesti?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Poista viesti?"; + +/* No comment provided by engineer. */ +"Delete messages" = "Poista viestit"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Poista viestit tämän jälkeen"; + +/* No comment provided by engineer. */ +"Delete old database" = "Poista vanha tietokanta"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Poista vanha tietokanta?"; + +/* No comment provided by engineer. */ +"Delete pending connection" = "Poista vireillä oleva yhteys"; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Poistetaanko odottava yhteys?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Poista profiili"; + +/* server test step */ +"Delete queue" = "Poista jono"; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Poista käyttäjäprofiili?"; + +/* deleted chat item */ +"deleted" = "poistettu"; + +/* No comment provided by engineer. */ +"Deleted at" = "Poistettu klo"; + +/* copied message info */ +"Deleted at: %@" = "Poistettu klo: %@"; + +/* rcv group event chat item */ +"deleted group" = "poistettu ryhmä"; + +/* No comment provided by engineer. */ +"Delivery" = "Toimitus"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Toimituskuittaukset poissa käytöstä!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Toimituskuittaukset!"; + +/* No comment provided by engineer. */ +"Description" = "Kuvaus"; + +/* No comment provided by engineer. */ +"Develop" = "Kehitä"; + +/* No comment provided by engineer. */ +"Developer tools" = "Kehittäjätyökalut"; + +/* No comment provided by engineer. */ +"Device" = "Laite"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Laitteen todennus on poistettu käytöstä. SimpleX Lock kytketään pois päältä."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Laitteen todennus ei ole käytössä. Voit ottaa SimpleX Lockin käyttöön Asetuksista, kun olet ottanut laitteen todennuksen käyttöön."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "eri siirtyminen sovelluksessa/tietokannassa: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Eri nimet, avatarit ja kuljetuseristys."; + +/* connection level description */ +"direct" = "suora"; + +/* chat feature */ +"Direct messages" = "Yksityisviestit"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this group." = "Yksityisviestit jäsenten välillä ovat kiellettyjä tässä ryhmässä."; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "Poista käytöstä (pidä ohitukset)"; + +/* No comment provided by engineer. */ +"Disable for all" = "Poista käytöstä kaikilta"; + +/* authentication reason */ +"Disable SimpleX Lock" = "Poista SimpleX Lock käytöstä"; + +/* No comment provided by engineer. */ +"disabled" = "ei käytössä"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Tuhoutuva viesti"; + +/* chat feature */ +"Disappearing messages" = "Tuhoutuvat viestit"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Katoavat viestit ovat kiellettyjä tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this group." = "Katoavat viestit ovat kiellettyjä tässä ryhmässä."; + +/* No comment provided by engineer. */ +"Disappears at" = "Katoaa klo"; + +/* copied message info */ +"Disappears at: %@" = "Katoaa klo: %@"; + +/* server test step */ +"Disconnect" = "Katkaise"; + +/* No comment provided by engineer. */ +"Display name" = "Näyttönimi"; + +/* No comment provided by engineer. */ +"Display name:" = "Näyttönimi:"; + +/* No comment provided by engineer. */ +"Do it later" = "Tee myöhemmin"; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "Älä käytä SimpleX-sovellusta hätäpuheluihin."; + +/* No comment provided by engineer. */ +"Don't create address" = "Älä luo osoitetta"; + +/* No comment provided by engineer. */ +"Don't enable" = "Älä salli"; + +/* No comment provided by engineer. */ +"Don't show again" = "Älä näytä uudelleen"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Alenna ja avaa keskustelu"; + +/* server test step */ +"Download file" = "Lataa tiedosto"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Päällekkäinen näyttönimi!"; + +/* integrity error chat item */ +"duplicate message" = "päällekkäinen viesti"; + +/* No comment provided by engineer. */ +"Duration" = "Kesto"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "e2e-salattu"; + +/* chat item action */ +"Edit" = "Muokkaa"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Muokkaa ryhmäprofiilia"; + +/* No comment provided by engineer. */ +"Enable" = "Salli"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Salli (pidä ohitukset)"; + +/* No comment provided by engineer. */ +"Enable automatic message deletion?" = "Ota automaattinen viestien poisto käyttöön?"; + +/* No comment provided by engineer. */ +"Enable for all" = "Salli kaikille"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Salli välittömät ilmoitukset?"; + +/* No comment provided by engineer. */ +"Enable lock" = "Ota lukitus käyttöön"; + +/* No comment provided by engineer. */ +"Enable notifications" = "Salli ilmoitukset"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "Salli säännölliset ilmoitukset?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Ota itsetuho käyttöön"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Ota itsetuhoava pääsykoodi käyttöön"; + +/* authentication reason */ +"Enable SimpleX Lock" = "Ota SimpleX Lock käyttöön"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "Ota TCP-säilytys käyttöön"; + +/* enabled status */ +"enabled" = "käytössä"; + +/* enabled status */ +"enabled for contact" = "käytössä kontaktille"; + +/* enabled status */ +"enabled for you" = "käytössä sinulle"; + +/* No comment provided by engineer. */ +"Encrypt" = "Salaa"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Salaa tietokanta?"; + +/* No comment provided by engineer. */ +"Encrypt local files" = "Salaa paikalliset tiedostot"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Salattu tietokanta"; + +/* notification */ +"Encrypted message or another event" = "Salattu viesti tai muu tapahtuma"; + +/* notification */ +"Encrypted message: database error" = "Salattu viesti: tietokantavirhe"; + +/* notification */ +"Encrypted message: database migration error" = "Salattu viesti: tietokannan siirtovirhe"; + +/* notification */ +"Encrypted message: keychain error" = "Salattu viesti: avainnipun virhe"; + +/* notification */ +"Encrypted message: no passphrase" = "Salattu viesti: ei tunnuslausetta"; + +/* notification */ +"Encrypted message: unexpected error" = "Salattu viesti: odottamaton virhe"; + +/* chat item text */ +"encryption agreed" = "salaus sovittu"; + +/* chat item text */ +"encryption agreed for %@" = "salaus sovittu %@:lle"; + +/* chat item text */ +"encryption ok" = "salaus ok"; + +/* chat item text */ +"encryption ok for %@" = "salaus ok %@:lle"; + +/* chat item text */ +"encryption re-negotiation allowed" = "salauksen uudelleenneuvottelu sallittu"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "salauksen uudelleenneuvottelu sallittu %@:lle"; + +/* chat item text */ +"encryption re-negotiation required" = "tarvitaan salauksen uudelleenneuvottelu"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "tarvitaan salauksen uudelleenneuvottelu %@:lle"; + +/* No comment provided by engineer. */ +"ended" = "päättyi"; + +/* call status */ +"ended call %@" = "puhelu päättyi %@:lle"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Anna oikea tunnuslause."; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Syötä pääsykoodi"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Syötä tunnuslause…"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "Kirjoita yllä oleva salasana näyttääksesi!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "Syötä palvelin manuaalisesti"; + +/* placeholder */ +"Enter welcome message…" = "Kirjoita tervetuloviesti…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Kirjoita tervetuloviesti... (valinnainen)"; + +/* No comment provided by engineer. */ +"error" = "virhe"; + +/* No comment provided by engineer. */ +"Error" = "Virhe"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Virhe osoitteenmuutoksen keskeytyksessä"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Virhe kontaktipyynnön hyväksymisessä"; + +/* No comment provided by engineer. */ +"Error accessing database file" = "Virhe tietokantatiedoston käyttämisessä"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Virhe lisättäessä jäseniä"; + +/* No comment provided by engineer. */ +"Error changing address" = "Virhe osoitteenvaihdossa"; + +/* No comment provided by engineer. */ +"Error changing role" = "Virhe roolin vaihdossa"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Virhe asetuksen muuttamisessa"; + +/* No comment provided by engineer. */ +"Error creating address" = "Virhe osoitteen luomisessa"; + +/* No comment provided by engineer. */ +"Error creating group" = "Virhe ryhmän luomisessa"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Virhe ryhmälinkin luomisessa"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Virhe profiilin luomisessa!"; + +/* No comment provided by engineer. */ +"Error decrypting file" = "Virhe tiedoston salauksen purussa"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Virhe keskustelujen tietokannan poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Virhe keskutelun poistamisessa!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Virhe yhteyden poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting contact" = "Virhe kontaktin poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Virhe tietokannan poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Virhe vanhan tietokannan poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Virhe tokenin poistamisessa"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Virhe käyttäjäprofiilin poistamisessa"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Virhe toimituskuittauksien sallimisessa!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Virhe ilmoitusten käyttöönotossa"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Virhe tietokannan salauksessa"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Virhe vietäessä keskustelujen tietokantaa"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Virhe keskustelujen tietokannan tuonnissa"; + +/* No comment provided by engineer. */ +"Error joining group" = "Virhe ryhmään liittymisessä"; + +/* No comment provided by engineer. */ +"Error loading %@ servers" = "Virhe %@-palvelimien lataamisessa"; + +/* No comment provided by engineer. */ +"Error receiving file" = "Virhe tiedoston vastaanottamisessa"; + +/* No comment provided by engineer. */ +"Error removing member" = "Virhe poistettaessa jäsentä"; + +/* No comment provided by engineer. */ +"Error saving %@ servers" = "Virhe %@ palvelimien tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Virhe ryhmäprofiilin tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "Virhe ICE-palvelimien tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Virhe pääsykoodin tallentamisessa"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Virhe tunnuslauseen tallentamisessa avainnippuun"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Virhe käyttäjän salasanan tallentamisessa"; + +/* No comment provided by engineer. */ +"Error sending email" = "Virhe sähköpostin lähettämisessä"; + +/* No comment provided by engineer. */ +"Error sending message" = "Virhe viestin lähettämisessä"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Virhe toimituskuittauksien asettamisessa!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Virhe käynnistettäessä keskustelua"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Virhe keskustelun lopettamisessa"; + +/* No comment provided by engineer. */ +"Error switching profile!" = "Virhe profiilin vaihdossa!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Virhe yhteyden synkronoinnissa"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Virhe ryhmälinkin päivittämisessä"; + +/* No comment provided by engineer. */ +"Error updating message" = "Virhe viestin päivityksessä"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Virhe asetusten päivittämisessä"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Virhe päivitettäessä käyttäjän tietosuojaa"; + +/* No comment provided by engineer. */ +"Error: " = "Virhe: "; + +/* No comment provided by engineer. */ +"Error: %@" = "Virhe: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Virhe: ei tietokantatiedostoa"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Virhe: URL on virheellinen"; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Jopa kun ei käytössä keskustelussa."; + +/* No comment provided by engineer. */ +"event happened" = "tapahtuma tapahtui"; + +/* No comment provided by engineer. */ +"Exit without saving" = "Poistu tallentamatta"; + +/* No comment provided by engineer. */ +"Export database" = "Vie tietokanta"; + +/* No comment provided by engineer. */ +"Export error:" = "Vientivirhe:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Viety tietokanta-arkisto."; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Tietokanta-arkiston vienti…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Tunnuslauseen poisto epäonnistui"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!"; + +/* No comment provided by engineer. */ +"Favorite" = "Suosikki"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "Tiedosto poistetaan palvelimilta."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "Tiedosto vastaanotetaan, kun kontaktisi on ladannut sen."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "Tiedosto vastaanotetaan, kun kontakti on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"File: %@" = "Tiedosto: %@"; + +/* No comment provided by engineer. */ +"Files & media" = "Tiedostot & media"; + +/* chat feature */ +"Files and media" = "Tiedostot ja media"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "Tiedostot ja media ovat tässä ryhmässä kiellettyjä."; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Tiedostot ja media kielletty!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Suodata lukemattomia- ja suosikkikeskusteluja."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Vihdoinkin meillä! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Löydä keskustelut nopeammin"; + +/* No comment provided by engineer. */ +"Fix" = "Korjaa"; + +/* No comment provided by engineer. */ +"Fix connection" = "Korjaa yhteys"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Korjaa yhteys?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Korjaa salaus varmuuskopioiden palauttamisen jälkeen."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Kontakti ei tue korjausta"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Ryhmän jäsen ei tue korjausta"; + +/* No comment provided by engineer. */ +"For console" = "Konsoliin"; + +/* No comment provided by engineer. */ +"French interface" = "Ranskalainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"Full link" = "Koko linkki"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Koko nimi (valinnainen)"; + +/* No comment provided by engineer. */ +"Full name:" = "Koko nimi:"; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Täysin uudistettu - toimii taustalla!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Entistä pienempi akun käyttö"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIFit ja tarrat"; + +/* No comment provided by engineer. */ +"Group" = "Ryhmä"; + +/* No comment provided by engineer. */ +"group deleted" = "ryhmä poistettu"; + +/* No comment provided by engineer. */ +"Group display name" = "Ryhmän näyttönimi"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "Ryhmän näyttönimi (valinnainen)"; + +/* No comment provided by engineer. */ +"Group image" = "Ryhmäkuva"; + +/* No comment provided by engineer. */ +"Group invitation" = "Ryhmän kutsu"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "Vanhentunut ryhmäkutsu"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "Ryhmäkutsu ei ole enää voimassa, lähettäjä poisti sen."; + +/* No comment provided by engineer. */ +"Group link" = "Ryhmälinkki"; + +/* No comment provided by engineer. */ +"Group links" = "Ryhmälinkit"; + +/* No comment provided by engineer. */ +"Group members can add message reactions." = "Ryhmän jäsenet voivat lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Group members can irreversibly delete sent messages." = "Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti."; + +/* No comment provided by engineer. */ +"Group members can send direct messages." = "Ryhmän jäsenet voivat lähettää suoraviestejä."; + +/* No comment provided by engineer. */ +"Group members can send disappearing messages." = "Ryhmän jäsenet voivat lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Group members can send files and media." = "Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa."; + +/* No comment provided by engineer. */ +"Group members can send voice messages." = "Ryhmän jäsenet voivat lähettää ääniviestejä."; + +/* notification */ +"Group message:" = "Ryhmäviesti:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Ryhmän moderointi"; + +/* No comment provided by engineer. */ +"Group preferences" = "Ryhmän asetukset"; + +/* No comment provided by engineer. */ +"Group profile" = "Ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Ryhmäprofiili tallennetaan jäsenten laitteille, ei palvelimille."; + +/* snd group event chat item */ +"group profile updated" = "ryhmäprofiili päivitetty"; + +/* No comment provided by engineer. */ +"Group welcome message" = "Ryhmän tervetuloviesti"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Ryhmä poistetaan kaikilta jäseniltä - tätä ei voi kumota!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "Ryhmä poistetaan sinulta - tätä ei voi perua!"; + +/* No comment provided by engineer. */ +"Help" = "Apua"; + +/* No comment provided by engineer. */ +"Hidden" = "Piilotettu"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Piilotetut keskusteluprofiilit"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Piilotettu profiilin salasana"; + +/* chat item action */ +"Hide" = "Piilota"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Piilota sovellusnäyttö viimeisimmissä sovelluksissa."; + +/* No comment provided by engineer. */ +"Hide profile" = "Piilota profiili"; + +/* No comment provided by engineer. */ +"Hide:" = "Piilota:"; + +/* No comment provided by engineer. */ +"History" = "Historia"; + +/* time unit */ +"hours" = "tuntia"; + +/* No comment provided by engineer. */ +"How it works" = "Kuinka se toimii"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "Miten SimpleX toimii"; + +/* No comment provided by engineer. */ +"How to" = "Miten"; + +/* No comment provided by engineer. */ +"How to use it" = "Kuinka sitä käytetään"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Miten käytät palvelimiasi"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "ICE-palvelimet (yksi per rivi)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki."; + +/* No comment provided by engineer. */ +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Jos et voi tavata henkilökohtaisesti, voit **skannata QR-koodin videopuhelussa** tai kontaktisi voi jakaa kutsulinkin."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Jos syötät tämän pääsykoodin sovellusta avatessasi, kaikki sovelluksen tiedot poistetaan peruuttamattomasti!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "Jos syötät itsetuhoutuvan pääsykoodin sovellusta avattaessa:"; + +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Jos haluat käyttää keskustelua nyt, napauta **Tee se myöhemmin** alla (sinulle tarjotaan tietokannan siirtämistä, kun käynnistät sovelluksen uudelleen)."; + +/* No comment provided by engineer. */ +"Ignore" = "Sivuuta"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "Kuva vastaanotetaan, kun kontaktisi on ladannut sen."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"Immediately" = "Heti"; + +/* No comment provided by engineer. */ +"Immune to spam and abuse" = "Immuuni roskapostille ja väärinkäytöksille"; + +/* No comment provided by engineer. */ +"Import" = "Tuo"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Tuo keskustelujen-tietokanta?"; + +/* No comment provided by engineer. */ +"Import database" = "Tuo tietokanta"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Parannettu yksityisyys ja turvallisuus"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Parannettu palvelimen kokoonpano"; + +/* No comment provided by engineer. */ +"In reply to" = "Vastauksena"; + +/* No comment provided by engineer. */ +"Incognito" = "Incognito"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Incognito-tila"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Incognito-tila suojaa yksityisyyttäsi käyttämällä uutta satunnaista profiilia jokaiselle kontaktille."; + +/* chat list item description */ +"incognito via contact address link" = "incognito kontaktilinkin kautta"; + +/* chat list item description */ +"incognito via group link" = "incognito ryhmälinkin kautta"; + +/* chat list item description */ +"incognito via one-time link" = "incognito kertalinkillä"; + +/* notification */ +"Incoming audio call" = "Saapuva äänipuhelu"; + +/* notification */ +"Incoming call" = "Saapuva puhelu"; + +/* notification */ +"Incoming video call" = "Saapuva videopuhelu"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Yhteensopimaton tietokantaversio"; + +/* PIN entry */ +"Incorrect passcode" = "Väärä pääsykoodi"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Väärä turvakoodi!"; + +/* connection level description */ +"indirect (%d)" = "epäsuora (%d)"; + +/* chat item action */ +"Info" = "Tiedot"; + +/* No comment provided by engineer. */ +"Initial role" = "Alkuperäinen rooli"; + +/* No comment provided by engineer. */ +"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Asenna [SimpleX Chat terminaalille](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Välittömät push-ilmoitukset ovat piilossa!\n"; + +/* No comment provided by engineer. */ +"Instantly" = "Heti"; + +/* No comment provided by engineer. */ +"Interface" = "Käyttöliittymä"; + +/* invalid chat data */ +"invalid chat" = "virheellinen keskustelu"; + +/* No comment provided by engineer. */ +"invalid chat data" = "virheelliset keskustelu-tiedot"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Virheellinen yhteyslinkki"; + +/* invalid chat item */ +"invalid data" = "virheelliset tiedot"; + +/* No comment provided by engineer. */ +"Invalid server address!" = "Virheellinen palvelinosoite!"; + +/* item status text */ +"Invalid status" = "Virheellinen tila"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "Vanhentunut kutsu!"; + +/* group name */ +"invitation to group %@" = "kutsu ryhmään %@"; + +/* No comment provided by engineer. */ +"Invite friends" = "Kutsu ystäviä"; + +/* No comment provided by engineer. */ +"Invite members" = "Kutsu jäseniä"; + +/* No comment provided by engineer. */ +"Invite to group" = "Kutsu ryhmään"; + +/* No comment provided by engineer. */ +"invited" = "kutsuttu"; + +/* rcv group event chat item */ +"invited %@" = "kutsuttu %@"; + +/* chat list item title */ +"invited to connect" = "kutsuttu yhteydenpitoon"; + +/* rcv group event chat item */ +"invited via your group link" = "kutsuttu ryhmäsi linkin kautta"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen - se mahdollistaa push-ilmoitusten vastaanottamisen."; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen sen muuttamisen tai sovelluksen uudelleen käynnistämisen jälkeen - se mahdollistaa push-ilmoitusten vastaanottamisen."; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Peruuttamaton viestin poisto"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "Viestien peruuttamaton poisto on kielletty tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this group." = "Viestien peruuttamaton poisto on kielletty tässä ryhmässä."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Se voi tapahtua, kun sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta."; + +/* No comment provided by engineer. */ +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Se voi tapahtua, kun:\n1. Viestit vanhenivat lähettävässä päätelaitteessa kahden päivän päästä tai palvelimella 30 päivän kuluttua.\n2. Viestin salauksen purku epäonnistui, koska sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta.\n3. Yhteys vaarantui."; + +/* No comment provided by engineer. */ +"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "Italialainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"italic" = "kursivoitu"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Japanilainen käyttöliittymä"; + +/* No comment provided by engineer. */ +"Join" = "Liity"; + +/* No comment provided by engineer. */ +"join as %@" = "Liity %@:nä"; + +/* No comment provided by engineer. */ +"Join group" = "Liity ryhmään"; + +/* No comment provided by engineer. */ +"Join incognito" = "Liity incognito-tilassa"; + +/* No comment provided by engineer. */ +"Joining group" = "Liittyy ryhmään"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Pidä kontaktisi"; + +/* No comment provided by engineer. */ +"Keychain error" = "Avainnipun virhe"; + +/* No comment provided by engineer. */ +"KeyChain error" = "Avainnipun virhe"; + +/* No comment provided by engineer. */ +"Large file!" = "Suuri tiedosto!"; + +/* No comment provided by engineer. */ +"Learn more" = "Lue lisää"; + +/* No comment provided by engineer. */ +"Leave" = "Poistu"; + +/* No comment provided by engineer. */ +"Leave group" = "Poistu ryhmästä"; + +/* No comment provided by engineer. */ +"Leave group?" = "Poistu ryhmästä?"; + +/* rcv group event chat item */ +"left" = "poistunut"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Jutellaan SimpleX Chatissa"; + +/* No comment provided by engineer. */ +"Light" = "Vaalea"; + +/* No comment provided by engineer. */ +"Limitations" = "Rajoitukset"; + +/* No comment provided by engineer. */ +"LIVE" = "LIVE"; + +/* No comment provided by engineer. */ +"Live message!" = "Live-viesti!"; + +/* No comment provided by engineer. */ +"Live messages" = "Live-viestit"; + +/* No comment provided by engineer. */ +"Local name" = "Paikallinen nimi"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Vain paikalliset profiilitiedot"; + +/* No comment provided by engineer. */ +"Lock after" = "Lukitse jälkeen"; + +/* No comment provided by engineer. */ +"Lock mode" = "Lukitustila"; + +/* No comment provided by engineer. */ +"Make a private connection" = "Luo yksityinen yhteys"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Hävitä yksi viesti"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Tee profiilista yksityinen!"; + +/* No comment provided by engineer. */ +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Varmista, että %@-palvelinosoitteet ovat oikeassa muodossa, että ne on erotettu toisistaan riveittäin ja että ne eivät ole päällekkäisiä (%@)."; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä."; + +/* No comment provided by engineer. */ +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Monet ihmiset kysyivät: *Jos SimpleX:llä ei ole käyttäjätunnuksia, miten se voi toimittaa viestejä?*"; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Merkitse poistetuksi kaikilta"; + +/* No comment provided by engineer. */ +"Mark read" = "Merkitse luetuksi"; + +/* No comment provided by engineer. */ +"Mark verified" = "Merkitse vahvistetuksi"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Markdown viesteissä"; + +/* marked deleted chat item preview text */ +"marked deleted" = "merkitty poistetuksi"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Enintään 30 sekuntia, vastaanotetaan välittömästi."; + +/* member role */ +"member" = "jäsen"; + +/* No comment provided by engineer. */ +"Member" = "Jäsen"; + +/* rcv group event chat item */ +"member connected" = "yhdistetty"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Jäsenen rooli muuttuu muotoon \"%@\". Kaikille ryhmän jäsenille ilmoitetaan asiasta."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Jäsenen rooli muutetaan muotoon \"%@\". Jäsen saa uuden kutsun."; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Jäsen poistetaan ryhmästä - tätä ei voi perua!"; + +/* item status text */ +"Message delivery error" = "Viestin toimitusvirhe"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Viestien toimituskuittaukset!"; + +/* No comment provided by engineer. */ +"Message draft" = "Viestiluonnos"; + +/* chat feature */ +"Message reactions" = "Viestireaktiot"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "Viestireaktiot ovat kiellettyjä tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "Viestireaktiot ovat kiellettyjä tässä ryhmässä."; + +/* notification */ +"message received" = "viesti vastaanotettu"; + +/* No comment provided by engineer. */ +"Message text" = "Viestin teksti"; + +/* No comment provided by engineer. */ +"Messages" = "Viestit"; + +/* No comment provided by engineer. */ +"Messages & files" = "Viestit ja tiedostot"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Siirretään tietokannan arkistoa…"; + +/* No comment provided by engineer. */ +"Migration error:" = "Siirtovirhe:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Siirto epäonnistui. Jatka nykyisen tietokannan käyttöä napauttamalla alla **Poistu**. Ilmoita ongelmasta sovelluskehittäjille keskustelussa tai sähköpostitse [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Siirto on valmis"; + +/* No comment provided by engineer. */ +"Migrations: %@" = "Siirrot: %@"; + +/* time unit */ +"minutes" = "minuuttia"; + +/* call status */ +"missed call" = "vastaamaton puhelu"; + +/* chat item action */ +"Moderate" = "Moderoi"; + +/* moderated chat item */ +"moderated" = "moderoitu"; + +/* No comment provided by engineer. */ +"Moderated at" = "Moderoitu klo"; + +/* copied message info */ +"Moderated at: %@" = "Moderoitu klo: %@"; + +/* No comment provided by engineer. */ +"moderated by %@" = "%@ moderoi"; + +/* time unit */ +"months" = "kuukautta"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Lisää parannuksia on tulossa pian!"; + +/* item status description */ +"Most likely this connection is deleted." = "Todennäköisesti tämä yhteys on poistettu."; + +/* No comment provided by engineer. */ +"Most likely this contact has deleted the connection with you." = "Todennäköisesti tämä kontakti on poistanut yhteyden sinuun."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Useita keskusteluprofiileja"; + +/* No comment provided by engineer. */ +"Mute" = "Mykistä"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Mykistetty ei-aktiivisena!"; + +/* No comment provided by engineer. */ +"Name" = "Nimi"; + +/* No comment provided by engineer. */ +"Network & servers" = "Verkko ja palvelimet"; + +/* No comment provided by engineer. */ +"Network settings" = "Verkkoasetukset"; + +/* No comment provided by engineer. */ +"Network status" = "Verkon tila"; + +/* No comment provided by engineer. */ +"never" = "ei koskaan"; + +/* notification */ +"New contact request" = "Uusi kontaktipyyntö"; + +/* notification */ +"New contact:" = "Uusi kontakti:"; + +/* No comment provided by engineer. */ +"New database archive" = "Uusi tietokanta-arkisto"; + +/* No comment provided by engineer. */ +"New display name" = "Uusi näyttönimi"; + +/* No comment provided by engineer. */ +"New in %@" = "Uutta %@"; + +/* No comment provided by engineer. */ +"New member role" = "Uusi jäsenrooli"; + +/* notification */ +"new message" = "uusi viesti"; + +/* notification */ +"New message" = "Uusi viesti"; + +/* No comment provided by engineer. */ +"New Passcode" = "Uusi pääsykoodi"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Uusi tunnuslause…"; + +/* pref value */ +"no" = "ei"; + +/* No comment provided by engineer. */ +"No" = "Ei"; + +/* Authentication unavailable */ +"No app password" = "Ei sovelluksen salasanaa"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Kontakteja ei ole valittu"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Ei lisättäviä kontakteja"; + +/* No comment provided by engineer. */ +"No delivery information" = "Ei toimitustietoja"; + +/* No comment provided by engineer. */ +"No device token!" = "Ei laitetunnusta!"; + +/* No comment provided by engineer. */ +"no e2e encryption" = "ei e2e-salausta"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Ei suodatettuja keskusteluja"; + +/* No comment provided by engineer. */ +"No group!" = "Ryhmää ei löydy!"; + +/* No comment provided by engineer. */ +"No history" = "Ei historiaa"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Ei lupaa ääniviestin tallentamiseen"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Ei vastaanotettuja tai lähetettyjä tiedostoja"; + +/* copied message info in history */ +"no text" = "ei tekstiä"; + +/* No comment provided by engineer. */ +"Notifications" = "Ilmoitukset"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Ilmoitukset on poistettu käytöstä!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Nyt järjestelmänvalvojat voivat:\n- poistaa jäsenten viestit.\n- poista jäsenet käytöstä (\"tarkkailija\" rooli)"; + +/* member role */ +"observer" = "tarkkailija"; + +/* enabled status + group pref value */ +"off" = "pois"; + +/* No comment provided by engineer. */ +"Off" = "Pois"; + +/* No comment provided by engineer. */ +"Off (Local)" = "Pois (Paikallinen)"; + +/* feature offered item */ +"offered %@" = "tarjottu %@"; + +/* feature offered item */ +"offered %@: %@" = "tarjottu %1$@: %2$@"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Old database" = "Vanha tietokanta"; + +/* No comment provided by engineer. */ +"Old database archive" = "Vanha tietokanta-arkisto"; + +/* group pref value */ +"on" = "päällä"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "Kertakutsulinkki"; + +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Onion-isäntiä ei käytetä."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**."; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Vain ryhmän omistajat voivat sallia tiedostoja ja mediaa."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Vain ryhmän omistajat voivat ottaa ääniviestit käyttöön."; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Vain sinä voit lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Vain sinä voit poistaa viestejä peruuttamattomasti (kontaktisi voi merkitä ne poistettavaksi)."; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Vain sinä voit soittaa puheluita."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Vain sinä voit lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Vain sinä voit lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Vain kontaktisi voi lisätä viestireaktioita."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Vain kontaktisi voi poistaa viestejä peruuttamattomasti (voit merkitä ne poistettavaksi)."; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Vain kontaktisi voi soittaa puheluita."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Vain kontaktisi voi lähettää katoavia viestejä."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Vain kontaktisi voi lähettää ääniviestejä."; + +/* No comment provided by engineer. */ +"Open chat" = "Avaa keskustelu"; + +/* authentication reason */ +"Open chat console" = "Avaa keskustelukonsoli"; + +/* No comment provided by engineer. */ +"Open Settings" = "Avaa Asetukset"; + +/* authentication reason */ +"Open user profiles" = "Avaa käyttäjäprofiilit"; + +/* No comment provided by engineer. */ +"Open-source protocol and code – anybody can run the servers." = "Avoimen lähdekoodin protokolla ja koodi - kuka tahansa voi käyttää palvelimia."; + +/* No comment provided by engineer. */ +"Opening database…" = "Avataan tietokantaa…"; + +/* No comment provided by engineer. */ +"Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "Linkin avaaminen selaimessa voi heikentää yhteyden yksityisyyttä ja turvallisuutta. Epäluotetut SimpleX-linkit näkyvät punaisina."; + +/* No comment provided by engineer. */ +"or chat with the developers" = "tai keskustele kehittäjien kanssa"; + +/* member role */ +"owner" = "omistaja"; + +/* No comment provided by engineer. */ +"Passcode" = "Pääsykoodi"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "Pääsykoodi vaihdettu!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Pääsykoodin syöttö"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "Pääsykoodia ei ole muutettu!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "Pääsykoodi asetettu!"; + +/* No comment provided by engineer. */ +"Password to show" = "Salasana näytettäväksi"; + +/* No comment provided by engineer. */ +"Paste" = "Liitä"; + +/* No comment provided by engineer. */ +"Paste image" = "Liitä kuva"; + +/* No comment provided by engineer. */ +"Paste received link" = "Liitä vastaanotettu linkki"; + +/* placeholder */ +"Paste the link you received to connect with your contact." = "Liitä saamasi linkki, jonka avulla voit muodostaa yhteyden kontaktiisi."; + +/* No comment provided by engineer. */ +"peer-to-peer" = "vertais"; + +/* No comment provided by engineer. */ +"People can connect to you only via the links you share." = "Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta."; + +/* No comment provided by engineer. */ +"Periodically" = "Ajoittain"; + +/* message decrypt error item */ +"Permanent decryption error" = "Pysyvä salauksen purkuvirhe"; + +/* No comment provided by engineer. */ +"PING count" = "PING-määrä"; + +/* No comment provided by engineer. */ +"PING interval" = "PING-väli"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Pyydä kontaktiasi sallimaan ääniviestien lähettäminen."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Tarkista verkkoyhteytesi %@:lla ja yritä uudelleen."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Tarkista omasi ja kontaktin asetukset."; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Ota yhteyttä ryhmän ylläpitäjään."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Anna oikea nykyinen tunnuslause."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Anna edellinen salasana tietokannan varmuuskopion palauttamisen jälkeen. Tätä toimintoa ei voi kumota."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Muista tai säilytä se turvallisesti - kadonnutta pääsykoodia ei voi palauttaa!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Ilmoita siitä kehittäjille."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Käynnistä sovellus uudelleen ja siirrä tietokanta push-ilmoitusten ottamiseksi käyttöön."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Säilytä tunnuslause turvallisesti, ET pääse keskusteluihin, jos kadotat sen."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Säilytä tunnuslause turvallisesti, ET voi muuttaa sitä, jos kadotat sen."; + +/* No comment provided by engineer. */ +"Polish interface" = "Puolalainen käyttöliittymä"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Palvelimen osoitteen varmenteen sormenjälki on mahdollisesti virheellinen"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "Säilytä viimeinen viestiluonnos liitteineen."; + +/* No comment provided by engineer. */ +"Preset server" = "Esiasetettu palvelin"; + +/* No comment provided by engineer. */ +"Preset server address" = "Esiasetettu palvelimen osoite"; + +/* No comment provided by engineer. */ +"Preview" = "Esikatselu"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Yksityisyys ja turvallisuus"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "Yksityisyys uudelleen määritettynä"; + +/* No comment provided by engineer. */ +"Private filenames" = "Yksityiset tiedostonimet"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "Profiili- ja palvelinyhteydet"; + +/* No comment provided by engineer. */ +"Profile image" = "Profiilikuva"; + +/* No comment provided by engineer. */ +"Profile password" = "Profiilin salasana"; + +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "Profiilipäivitys lähetetään kontakteillesi."; + +/* No comment provided by engineer. */ +"Prohibit audio/video calls." = "Estä ääni- ja videopuhelut."; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "Estä peruuttamaton viestien poistaminen."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "Estä viestireaktiot."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "Estä viestireaktiot."; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Estä suorien viestien lähettäminen jäsenille."; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "Estä katoavien viestien lähettäminen."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Estä tiedostojen ja median lähettäminen."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "Estä ääniviestien lähettäminen."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Suojaa sovellusnäyttö"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Suojaa keskusteluprofiilisi salasanalla!"; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Protokollan aikakatkaisu"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Protokollan aikakatkaisu per KB"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push-ilmoitukset"; + +/* No comment provided by engineer. */ +"Rate the app" = "Arvioi sovellus"; + +/* chat item menu */ +"React…" = "Reagoi…"; + +/* No comment provided by engineer. */ +"Read" = "Lue"; + +/* No comment provided by engineer. */ +"Read more" = "Lue lisää"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Lue lisää [Käyttöoppaasta](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; + +/* No comment provided by engineer. */ +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Lue lisää [GitHub-arkistosta](https://github.com/simplex-chat/simplex-chat#readme)."; + +/* No comment provided by engineer. */ +"Read more in our GitHub repository." = "Lue lisää GitHub-tietovarastostamme."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "Kuittaukset pois käytöstä"; + +/* No comment provided by engineer. */ +"received answer…" = "vastaus saatu…"; + +/* No comment provided by engineer. */ +"Received at" = "Vastaanotettu klo"; + +/* copied message info */ +"Received at: %@" = "Vastaanotettu klo: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "vahvistus saatu…"; + +/* notification */ +"Received file event" = "Tiedoston vastaanottotapahtuma"; + +/* message info title */ +"Received message" = "Vastaanotettu viesti"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "Tiedoston vastaanotto pysäytetään."; + +/* No comment provided by engineer. */ +"Receiving via" = "Vastaanotto kautta"; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "Vastaanottajat näkevät päivitykset, kun kirjoitat niitä."; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Yhdistä kaikki yhdistetyt palvelimet uudelleen pakottaaksesi viestin toimituksen. Tämä käyttää ylimääräistä liikennettä."; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Yhdistä palvelimet uudelleen?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Tietue päivitetty klo"; + +/* copied message info */ +"Record updated at: %@" = "Tietue päivitetty klo: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Pienempi akun käyttö"; + +/* reject incoming call via notification */ +"Reject" = "Hylkää"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Hylkää (lähettäjälle EI ilmoiteta)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Hylkää yhteyspyyntö"; + +/* call status */ +"rejected call" = "hylätty puhelu"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Välityspalvelinta käytetään vain tarvittaessa. Toinen osapuoli voi tarkkailla IP-osoitettasi."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa."; + +/* No comment provided by engineer. */ +"Remove" = "Poista"; + +/* No comment provided by engineer. */ +"Remove member" = "Poista jäsen"; + +/* No comment provided by engineer. */ +"Remove member?" = "Poista jäsen?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Poista tunnuslause avainnipusta?"; + +/* No comment provided by engineer. */ +"removed" = "poistettu"; + +/* rcv group event chat item */ +"removed %@" = "%@ poistettu"; + +/* rcv group event chat item */ +"removed you" = "poisti sinut"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Neuvottele uudelleen"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Uudelleenneuvottele salaus"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Uudelleenneuvottele salaus?"; + +/* chat item action */ +"Reply" = "Vastaa"; + +/* No comment provided by engineer. */ +"Required" = "Pakollinen"; + +/* No comment provided by engineer. */ +"Reset" = "Oletustilaan"; + +/* No comment provided by engineer. */ +"Reset colors" = "Oletusvärit"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Palauta oletusasetukset"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Käynnistä sovellus uudelleen käyttääksesi tuotua keskustelujen-tietokantaa"; + +/* No comment provided by engineer. */ +"Restore" = "Palauta"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Palauta tietokannan varmuuskopio"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Palauta tietokannan varmuuskopio?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Virhe tietokannan palauttamisessa"; + +/* chat item action */ +"Reveal" = "Paljasta"; + +/* No comment provided by engineer. */ +"Revert" = "Palauta"; + +/* No comment provided by engineer. */ +"Revoke" = "Peruuta"; + +/* cancel file action */ +"Revoke file" = "Peruuta tiedosto"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Peruuta tiedosto?"; + +/* No comment provided by engineer. */ +"Role" = "Rooli"; + +/* No comment provided by engineer. */ +"Run chat" = "Käynnistä chat"; + +/* chat item action */ +"Save" = "Tallenna"; + +/* No comment provided by engineer. */ +"Save (and notify contacts)" = "Tallenna (ja ilmoita kontakteille)"; + +/* No comment provided by engineer. */ +"Save and notify contact" = "Tallenna ja ilmoita kontaktille"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "Tallenna ja ilmoita ryhmän jäsenille"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Tallenna ja päivitä ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Save archive" = "Tallenna arkisto"; + +/* No comment provided by engineer. */ +"Save auto-accept settings" = "Tallenna automaattisen hyväksynnän asetukset"; + +/* No comment provided by engineer. */ +"Save group profile" = "Tallenna ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Tallenna tunnuslause ja avaa keskustelu"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Tallenna tunnuslause Avainnippuun"; + +/* No comment provided by engineer. */ +"Save preferences?" = "Tallenna asetukset?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Tallenna profiilin salasana"; + +/* No comment provided by engineer. */ +"Save servers" = "Tallenna palvelimet"; + +/* No comment provided by engineer. */ +"Save servers?" = "Tallenna palvelimet?"; + +/* No comment provided by engineer. */ +"Save settings?" = "Tallenna asetukset?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Tallenna tervetuloviesti?"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "Tallennetut WebRTC ICE -palvelimet poistetaan"; + +/* No comment provided by engineer. */ +"Scan code" = "Skannaa koodi"; + +/* No comment provided by engineer. */ +"Scan QR code" = "Skannaa QR-koodi"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Skannaa turvakoodi kontaktisi sovelluksesta."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "Skannaa palvelimen QR-koodi"; + +/* No comment provided by engineer. */ +"Search" = "Haku"; + +/* network option */ +"sec" = "sek"; + +/* time unit */ +"seconds" = "sekuntia"; + +/* No comment provided by engineer. */ +"secret" = "salainen"; + +/* server test step */ +"Secure queue" = "Turvallinen jono"; + +/* No comment provided by engineer. */ +"Security assessment" = "Turvallisuusarviointi"; + +/* No comment provided by engineer. */ +"Security code" = "Turvakoodi"; + +/* chat item text */ +"security code changed" = "turvakoodi on muuttunut"; + +/* No comment provided by engineer. */ +"Select" = "Valitse"; + +/* No comment provided by engineer. */ +"Self-destruct" = "Itsetuho"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Itsetuhoutuva pääsykoodi"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Itsetuhoutuva pääsykoodi vaihdettu!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Itsetuhoutuva pääsykoodi käytössä!"; + +/* No comment provided by engineer. */ +"Send" = "Lähetä"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Lähetä live-viesti - se päivittyy vastaanottajille, kun kirjoitat sitä"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Lähetä toimituskuittaukset vastaanottajalle"; + +/* No comment provided by engineer. */ +"Send direct message" = "Lähetä yksityisviesti"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Lähetä katoava viesti"; + +/* No comment provided by engineer. */ +"Send link previews" = "Lähetä linkkien esikatselu"; + +/* No comment provided by engineer. */ +"Send live message" = "Lähetä live-viesti"; + +/* No comment provided by engineer. */ +"Send notifications" = "Lähetys ilmoitukset"; + +/* No comment provided by engineer. */ +"Send notifications:" = "Lähetys ilmoitukset:"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Lähetä kysymyksiä ja ideoita"; + +/* No comment provided by engineer. */ +"Send receipts" = "Lähetä kuittaukset"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Lähetä ne galleriasta tai mukautetuista näppäimistöistä."; + +/* No comment provided by engineer. */ +"Sender cancelled file transfer." = "Lähettäjä peruutti tiedoston siirron."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "Lähettäjä on saattanut poistaa yhteyspyynnön."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille näkyvissä keskusteluprofiileissa."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "Tiedoston lähettäminen lopetetaan."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "Kuittauksien lähettäminen ei ole käytössä %lld kontakteille"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Kuittien lähettäminen ei ole käytössä %lld ryhmille"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Kuittauksien lähettäminen on käytössä %lld kontakteille"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Kuittauksien lähettäminen on käytössä %lld ryhmille"; + +/* No comment provided by engineer. */ +"Sending via" = "Lähetetään kautta"; + +/* No comment provided by engineer. */ +"Sent at" = "Lähetetty klo"; + +/* copied message info */ +"Sent at: %@" = "Lähetetty klo: %@"; + +/* notification */ +"Sent file event" = "Lähetetty tiedosto tapahtuma"; + +/* message info title */ +"Sent message" = "Lähetetty viesti"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Lähetetyt viestit poistetaan asetetun ajan kuluttua."; + +/* server test error */ +"Server requires authorization to create queues, check password" = "Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana"; + +/* server test error */ +"Server requires authorization to upload, check password" = "Palvelin vaatii valtuutuksen tiedoston lataamiseksi, tarkista salasana"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Palvelintesti epäonnistui!"; + +/* No comment provided by engineer. */ +"Servers" = "Palvelimet"; + +/* No comment provided by engineer. */ +"Set 1 day" = "Aseta 1 päivä"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Aseta kontaktin nimi…"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Aseta ryhmän asetukset"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Aseta se järjestelmän todennuksen sijaan."; + +/* No comment provided by engineer. */ +"Set passcode" = "Aseta pääsykoodi"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Aseta tunnuslause vientiä varten"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Aseta uusille jäsenille näytettävä viesti!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Aseta aikakatkaisut välityspalvelimelle/VPN:lle"; + +/* No comment provided by engineer. */ +"Settings" = "Asetukset"; + +/* chat item action */ +"Share" = "Jaa"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Jaa kertakäyttölinkki"; + +/* No comment provided by engineer. */ +"Share address" = "Jaa osoite"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "Jaa osoite kontakteille?"; + +/* No comment provided by engineer. */ +"Share link" = "Jaa linkki"; + +/* No comment provided by engineer. */ +"Share one-time invitation link" = "Jaa kertakutsulinkki"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Jaa kontaktien kanssa"; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Näytä puhelut puhelinhistoriassa"; + +/* No comment provided by engineer. */ +"Show developer options" = "Näytä kehittäjävaihtoehdot"; + +/* No comment provided by engineer. */ +"Show last messages" = "Näytä viimeiset viestit"; + +/* No comment provided by engineer. */ +"Show preview" = "Näytä esikatselu"; + +/* No comment provided by engineer. */ +"Show:" = "Näytä:"; + +/* No comment provided by engineer. */ +"SimpleX address" = "SimpleX-osoite"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "SimpleX-osoite"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "Trail of Bits on tarkastanut SimpleX Chatin tietoturvan."; + +/* simplex link type */ +"SimpleX contact address" = "SimpleX-yhteystiedot"; + +/* notification */ +"SimpleX encrypted message or connection event" = "SimpleX-salattu viesti tai yhteystapahtuma"; + +/* simplex link type */ +"SimpleX group link" = "SimpleX-ryhmän linkki"; + +/* No comment provided by engineer. */ +"SimpleX links" = "SimpleX-linkit"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX Lock"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "SimpleX Lock -tila"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "SimpleX Lock ei ole käytössä!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX Lock päällä"; + +/* simplex link type */ +"SimpleX one-time invitation" = "SimpleX-kertakutsu"; + +/* No comment provided by engineer. */ +"Skip" = "Ohita"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Ohitetut viestit"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Pienryhmät (max 20)"; + +/* No comment provided by engineer. */ +"SMP servers" = "SMP-palvelimet"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja."; + +/* notification title */ +"Somebody" = "Joku"; + +/* No comment provided by engineer. */ +"Start a new chat" = "Aloita uusi keskustelu"; + +/* No comment provided by engineer. */ +"Start chat" = "Aloita keskustelu"; + +/* No comment provided by engineer. */ +"Start migration" = "Aloita siirto"; + +/* No comment provided by engineer. */ +"starting…" = "alkaa…"; + +/* No comment provided by engineer. */ +"Stop" = "Lopeta"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "Pysäytä keskustelu tietokantatoimien mahdollistamiseksi"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Lopeta keskustelu?"; + +/* cancel file action */ +"Stop file" = "Pysäytä tiedosto"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Lopeta tiedoston vastaanottaminen?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Lopeta tiedoston lähettäminen?"; + +/* No comment provided by engineer. */ +"Stop sharing" = "Lopeta jakaminen"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "Lopeta osoitteen jakaminen?"; + +/* authentication reason */ +"Stop SimpleX" = "Lopeta SimpleX"; + +/* No comment provided by engineer. */ +"strike" = "soita"; + +/* No comment provided by engineer. */ +"Submit" = "Lähetä"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "SimpleX Chat tuki"; + +/* No comment provided by engineer. */ +"System" = "Järjestelmä"; + +/* No comment provided by engineer. */ +"System authentication" = "Järjestelmän todennus"; + +/* No comment provided by engineer. */ +"Take picture" = "Ota kuva"; + +/* No comment provided by engineer. */ +"Tap button " = "Napauta painiketta "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Aktivoi profiili napauttamalla."; + +/* No comment provided by engineer. */ +"Tap to join" = "Liity napauttamalla"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "Napauta liittyäksesi incognito-tilassa"; + +/* No comment provided by engineer. */ +"Tap to start a new chat" = "Aloita uusi keskustelu napauttamalla"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "TCP-yhteyden aikakatkaisu"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* server test failure */ +"Test failed at step %@." = "Testi epäonnistui vaiheessa %@."; + +/* No comment provided by engineer. */ +"Test server" = "Testipalvelin"; + +/* No comment provided by engineer. */ +"Test servers" = "Testipalvelimet"; + +/* No comment provided by engineer. */ +"Tests failed!" = "Testit epäonnistuivat!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "Kiitos SimpleX Chatin asentamisesta!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Kiitos käyttäjille - [osallistu Weblaten avulla](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Kiitokset käyttäjille – osallistu Weblaten kautta!"; + +/* No comment provided by engineer. */ +"The 1st platform without any user identifiers – private by design." = "Ensimmäinen alusta ilman käyttäjätunnisteita – suunniteltu yksityiseksi."; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Hyväksymäsi yhteys peruuntuu!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "Kontakti, jolle jaoit tämän linkin, EI voi muodostaa yhteyttä!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Luotu arkisto on käytettävissä sovelluksen Asetukset / Tietokanta / Vanha tietokanta-arkisto kautta."; + +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!"; + +/* No comment provided by engineer. */ +"The group is fully decentralized – it is visible only to the members." = "Ryhmä on täysin hajautettu - se näkyy vain jäsenille."; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "Edellisen viestin tarkiste on erilainen."; + +/* No comment provided by engineer. */ +"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Seuraavan viestin tunnus on väärä (pienempi tai yhtä suuri kuin edellisen).\nTämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Viesti poistetaan kaikilta jäseniltä."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Viesti merkitään moderoiduksi kaikille jäsenille."; + +/* No comment provided by engineer. */ +"The next generation of private messaging" = "Seuraavan sukupolven yksityisviestit"; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Vanhaa tietokantaa ei poistettu siirron aikana, se voidaan kuitenkin poistaa."; + +/* No comment provided by engineer. */ +"The profile is only shared with your contacts." = "Profiili jaetaan vain kontaktiesi kanssa."; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Toinen kuittaus, joka uupui! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "Lähettäjälle EI ilmoiteta"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**."; + +/* No comment provided by engineer. */ +"Theme" = "Teema"; + +/* No comment provided by engineer. */ +"There should be at least one user profile." = "Käyttäjäprofiileja tulee olla vähintään yksi."; + +/* No comment provided by engineer. */ +"There should be at least one visible user profile." = "Näkyviä käyttäjäprofiileja tulee olla vähintään yksi."; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Nämä asetukset koskevat nykyistä profiiliasi **%@**."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa."; + +/* No comment provided by engineer. */ +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Tätä toimintoa ei voi kumota - kaikki vastaanotetut ja lähetetyt tiedostot ja media poistetaan. Matalan resoluution kuvat säilyvät."; + +/* No comment provided by engineer. */ +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Tätä toimintoa ei voi kumota - profiilisi, kontaktisi, viestisi ja tiedostosi poistuvat peruuttamattomasti."; + +/* notification title */ +"this contact" = "tämä kontakti"; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Tässä ryhmässä on yli %lld jäsentä, lähetyskuittauksia ei lähetetä."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Tätä ryhmää ei enää ole olemassa."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**."; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Voit esittää kysymyksiä ja saada päivityksiä:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "Kontaktisi voi muodostaa yhteyden skannaamalla QR-koodin tai käyttämällä sovelluksessa olevaa linkkiä."; + +/* No comment provided by engineer. */ +"To make a new connection" = "Uuden yhteyden luominen"; + +/* No comment provided by engineer. */ +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Yksityisyyden suojaamiseksi kaikkien muiden alustojen käyttämien käyttäjätunnusten sijaan SimpleX käyttää viestijonojen tunnisteita, jotka ovat kaikille kontakteille erillisiä."; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Suojaa tietosi ottamalla SimpleX Lock käyttöön.\nSinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus otetaan käyttöön."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "Jos haluat nauhoittaa ääniviestin, anna lupa käyttää mikrofonia."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Voit paljastaa piilotetun profiilisi syöttämällä koko salasanan hakukenttään **Keskusteluprofiilisi** -sivulla."; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Keskustelujen-tietokanta on siirrettävä välittömien push-ilmoitusten tukemiseksi."; + +/* No comment provided by engineer. */ +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia."; + +/* No comment provided by engineer. */ +"Transport isolation" = "Kuljetuksen eristäminen"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta."; + +/* No comment provided by engineer. */ +"Turn off" = "Sammuta"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "Kytke ilmoitukset pois päältä?"; + +/* No comment provided by engineer. */ +"Turn on" = "Kytke päälle"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Ääniviestiä ei voi tallentaa"; + +/* item status description */ +"Unexpected error: %@" = "Odottamaton virhe: %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Odottamaton siirtotila"; + +/* No comment provided by engineer. */ +"Unfav." = "Epäsuotuisa."; + +/* No comment provided by engineer. */ +"Unhide" = "Näytä"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Näytä keskusteluprofiili"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Näytä profiili"; + +/* No comment provided by engineer. */ +"Unit" = "Yksikkö"; + +/* connection info */ +"unknown" = "tuntematon"; + +/* callkit banner */ +"Unknown caller" = "Tuntematon soittaja"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Tuntematon tietokantavirhe: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Tuntematon virhe"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi."; + +/* No comment provided by engineer. */ +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Ellei yhteyshenkilösi poistanut yhteyttä tai tämä linkki oli jo käytössä, se voi olla virhe - ilmoita siitä.\nJos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja tarkista, että verkkoyhteytesi on vakaa."; + +/* No comment provided by engineer. */ +"Unlock" = "Avaa"; + +/* authentication reason */ +"Unlock app" = "Avaa sovellus"; + +/* No comment provided by engineer. */ +"Unmute" = "Poista mykistys"; + +/* No comment provided by engineer. */ +"Unread" = "Lukematon"; + +/* No comment provided by engineer. */ +"Update" = "Päivitä"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "Päivitä .onion-isäntien asetus?"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Päivitä tietokannan tunnuslause"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Päivitä verkkoasetukset?"; + +/* No comment provided by engineer. */ +"Update transport isolation mode?" = "Päivitä kuljetuksen eristystila?"; + +/* rcv group event chat item */ +"updated group profile" = "päivitetty ryhmäprofiili"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Päivitä ja avaa keskustelu"; + +/* server test step */ +"Upload file" = "Lataa tiedosto"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Käytä .onion-isäntiä"; + +/* No comment provided by engineer. */ +"Use chat" = "Käytä chattia"; + +/* No comment provided by engineer. */ +"Use current profile" = "Käytä nykyistä profiilia"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Käytä uusiin yhteyksiin"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "Käytä iOS:n puhelujen käyttöliittymää"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Käytä uutta incognito-profiilia"; + +/* No comment provided by engineer. */ +"Use server" = "Käytä palvelinta"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "Käytä SimpleX Chat palvelimia?"; + +/* No comment provided by engineer. */ +"User profile" = "Käyttäjäprofiili"; + +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = ".onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan."; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Käyttää SimpleX Chat -palvelimia."; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Tarkista yhteyden suojaus"; + +/* No comment provided by engineer. */ +"Verify security code" = "Tarkista turvakoodi"; + +/* No comment provided by engineer. */ +"Via browser" = "Selaimella"; + +/* chat list item description */ +"via contact address link" = "kontaktiosoitelinkillä"; + +/* chat list item description */ +"via group link" = "ryhmälinkillä"; + +/* chat list item description */ +"via one-time link" = "kertalinkillä"; + +/* No comment provided by engineer. */ +"via relay" = "releellä"; + +/* No comment provided by engineer. */ +"Video call" = "Videopuhelu"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "videopuhelu (ei e2e-salattu)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "Video vastaanotetaan, kun kontaktisi on ladannut sen."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "Video vastaanotetaan, kun kontaktisi on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "Videot ja tiedostot 1 Gt asti"; + +/* No comment provided by engineer. */ +"View security code" = "Näytä turvakoodi"; + +/* No comment provided by engineer. */ +"Voice message…" = "Ääniviesti…"; + +/* chat feature */ +"Voice messages" = "Ääniviestit"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "Ääniviestit ovat kiellettyjä tässä keskustelussa."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this group." = "Ääniviestit ovat kiellettyjä tässä ryhmässä."; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Ääniviestit kielletty!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "odottaa vastaamista…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "odottaa vahvistusta…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "Odottaa tiedostoa"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Odottaa kuvaa"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Odottaa videota"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "haluaa olla yhteydessä sinuun!"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Varoitus: saatat menettää joitain tietoja!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "WebRTC ICE -palvelimet"; + +/* time unit */ +"weeks" = "viikkoa"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Tervetuloa %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Tervetuloviesti"; + +/* No comment provided by engineer. */ +"What's new" = "Uusimmat"; + +/* No comment provided by engineer. */ +"When available" = "Kun saatavilla"; + +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "Kun ihmiset pyytävät yhteyden muodostamista, voit hyväksyä tai hylätä sen."; + +/* No comment provided by engineer. */ +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Kun jaat inkognitoprofiilin jonkun kanssa, tätä profiilia käytetään ryhmissä, joihin tämä sinut kutsuu."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "Valinnaisella tervetuloviestillä."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Väärä tietokannan tunnuslause"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Väärä tunnuslause!"; + +/* No comment provided by engineer. */ +"XFTP servers" = "XFTP-palvelimet"; + +/* pref value */ +"yes" = "kyllä"; + +/* No comment provided by engineer. */ +"You" = "Sinä"; + +/* No comment provided by engineer. */ +"You accepted connection" = "Hyväksyit yhteyden"; + +/* No comment provided by engineer. */ +"You allow" = "Sallit"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Sinulla on jo keskusteluprofiili samalla näyttönimellä. Valitse toinen nimi."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Olet jo muodostanut yhteyden %@:n kanssa."; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Olet yhteydessä palvelimeen, jota käytetään vastaanottamaan viestejä tältä kontaktilta."; + +/* No comment provided by engineer. */ +"you are invited to group" = "sinut on kutsuttu ryhmään"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Sinut on kutsuttu ryhmään"; + +/* No comment provided by engineer. */ +"you are observer" = "olet tarkkailija"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta."; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Voit myös muodostaa yhteyden klikkaamalla linkkiä. Jos se avautuu selaimessa, napsauta **Avaa mobiilisovelluksessa**-painiketta."; + +/* No comment provided by engineer. */ +"You can create it later" = "Voit luoda sen myöhemmin"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Voit ottaa käyttöön myöhemmin asetusten kautta"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; + +/* notification body */ +"You can now send messages to %@" = "Voit nyt lähettää viestejä %@:lle"; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista."; + +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Voit jakaa linkin tai QR-koodin - kuka tahansa voi liittyä ryhmään. Et menetä ryhmän jäseniä, jos poistat sen myöhemmin."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Voit jakaa tämän osoitteen kontaktiesi kanssa, jotta ne voivat muodostaa yhteyden **%@** kanssa."; + +/* No comment provided by engineer. */ +"You can share your address as a link or QR code - anybody can connect to you." = "Voit jakaa osoitteesi linkkinä tai QR-koodina - kuka tahansa voi muodostaa yhteyden sinuun."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen"; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "Voit ottaa SimpleX Lockin käyttöön Asetusten kautta."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Voit käyttää markdownia viestien muotoiluun:"; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Et voi lähettää viestejä!"; + +/* chat item text */ +"you changed address" = "muutit osoitetta"; + +/* chat item text */ +"you changed address for %@" = "muutit osoitetta %@:ksi"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "vaihdoit roolin itsellesi %@:ksi"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "olet vaihtanut %1$@:n roolin %2$@:ksi"; + +/* No comment provided by engineer. */ +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Sinä hallitset, minkä palvelim(i)en kautta **viestit vastaanotetaan**, kontaktisi - palvelimet, joita käytät viestien lähettämiseen niille."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Sinua ei voitu todentaa; yritä uudelleen."; + +/* No comment provided by engineer. */ +"You have no chats" = "Sinulla ei ole keskusteluja"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Kutsuit kontaktin"; + +/* No comment provided by engineer. */ +"You joined this group" = "Liityit tähän ryhmään"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Liityit tähän ryhmään. Muodostetaan yhteyttä ryhmän jäsenten kutsumiseksi."; + +/* snd group event chat item */ +"you left" = "lähdit"; + +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Sinun tulee käyttää keskustelujen-tietokannan uusinta versiota AINOSTAAN yhdessä laitteessa, muuten saatat lakata vastaanottamasta viestejä joiltakin kontakteilta."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Hylkäsit ryhmäkutsun"; + +/* snd group event chat item */ +"you removed %@" = "poistit %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Lähetit ryhmäkutsun"; + +/* chat list item description */ +"you shared one-time link" = "jaoit kertalinkin"; + +/* chat list item description */ +"you shared one-time link incognito" = "jaoit kertalinkin incognito-tilassa"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Sinut yhdistetään ryhmään, kun ryhmän isännän laite on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Sinut yhdistetään, kun yhteyspyyntösi on hyväksytty, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Sinut yhdistetään, kun kontaktisi laite on online-tilassa, odota tai tarkista myöhemmin!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Sinun on tunnistauduttava, kun käynnistät sovelluksen tai jatkat sen käyttöä 30 sekunnin tauon jälkeen."; + +/* No comment provided by engineer. */ +"You will join a group this link refers to and connect to its group members." = "Liityt ryhmään, johon tämä linkki viittaa, ja muodostat yhteyden sen ryhmän jäseniin."; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "Et enää saa viestejä tästä ryhmästä. Keskusteluhistoria säilytetään."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Et menetä kontaktejasi, jos poistat osoitteesi myöhemmin."; + +/* No comment provided by engineer. */ +"you: " = "sinä: "; + +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Yrität kutsua kontaktia, jonka kanssa olet jakanut inkognito-profiilin, ryhmään, jossa käytät pääprofiiliasi"; + +/* No comment provided by engineer. */ +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Käytät tässä ryhmässä incognito-profiilia. Kontaktien kutsuminen ei ole sallittua, jotta pääprofiilisi ei tule jaetuksi"; + +/* No comment provided by engineer. */ +"Your %@ servers" = "%@-palvelimesi"; + +/* No comment provided by engineer. */ +"Your calls" = "Puhelusi"; + +/* No comment provided by engineer. */ +"Your chat database" = "Keskustelut-tietokantasi"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "Keskustelut-tietokantasi ei ole salattu - aseta tunnuslause sen salaamiseksi."; + +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "Keskusteluprofiilisi lähetetään ryhmän jäsenille"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Keskusteluprofiilisi"; + +/* No comment provided by engineer. */ +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa.\nVoit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä)."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@)."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "Kontaktisi voivat sallia viestien täydellisen poistamisen."; + +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Kontaktisi SimpleX:ssä näkevät sen.\nVoit muuttaa sitä Asetuksista."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "Kontaktisi pysyvät yhdistettyinä."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Nykyinen keskustelut-tietokantasi poistetaan ja korvataan tuodulla tietokannalla."; + +/* No comment provided by engineer. */ +"Your current profile" = "Nykyinen profiilisi"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "ICE-palvelimesi"; + +/* No comment provided by engineer. */ +"Your preferences" = "Asetuksesi"; + +/* No comment provided by engineer. */ +"Your privacy" = "Yksityisyytesi"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Profiilisi **%@** jaetaan."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Profiilisi tallennetaan laitteeseesi ja jaetaan vain yhteystietojesi kanssa.\nSimpleX-palvelimet eivät näe profiiliasi."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "Profiilisi, kontaktisi ja toimitetut viestit tallennetaan laitteellesi."; + +/* No comment provided by engineer. */ +"Your random profile" = "Satunnainen profiilisi"; + +/* No comment provided by engineer. */ +"Your server" = "Palvelimesi"; + +/* No comment provided by engineer. */ +"Your server address" = "Palvelimesi osoite"; + +/* No comment provided by engineer. */ +"Your settings" = "Asetuksesi"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "SimpleX-osoitteesi"; + +/* No comment provided by engineer. */ +"Your SMP servers" = "SMP-palvelimesi"; + +/* No comment provided by engineer. */ +"Your XFTP servers" = "XFTP-palvelimesi"; + diff --git a/apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..969e43e449 --- /dev/null +++ b/apps/ios/fi.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,15 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX tarvitsee pääsyn kameraan, jotta se voi skannata QR-koodeja muodostaakseen yhteyden muihin käyttäjiin ja videopuheluita varten."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX käyttää Face ID:tä paikalliseen todennukseen"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX tarvitsee mikrofonia ääni- ja videopuheluita ja ääniviestien tallentamista varten."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX tarvitsee pääsyn valokuvakirjastoon kuvattujen ja vastaanotettujen medioiden tallentamista varten"; + diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 1bbd8bc465..f9f7703382 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_italique_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- connexion au [service d'annuaire](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA) !\n- les accusés de réception (jusqu'à 20 membres).\n- plus rapide et plus stable."; + /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- une diffusion plus stable des messages.\n- des groupes un peu plus performants.\n- et bien d'autres choses encore !"; @@ -88,6 +91,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*gras*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Historique"; + +/* copied message info */ +"## In reply to" = "## En réponse à"; + /* No comment provided by engineer. */ "#secret#" = "#secret#"; @@ -106,6 +118,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ et %@ sont connecté.es"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ à %2$@ :"; @@ -124,6 +139,9 @@ /* notification title */ "%@ wants to connect!" = "%@ veut se connecter !"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ et %lld autres membres sont connectés"; + /* copied message info */ "%@:" = "%@ :"; @@ -166,6 +184,9 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld minutes"; +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld nouvelles langues d'interface"; + /* No comment provided by engineer. */ "%lld second(s)" = "%lld seconde·s"; @@ -245,10 +266,7 @@ "A new contact" = "Un nouveau contact"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Un profil aléatoire sera envoyé au contact qui vous a envoyé ce lien"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Un profil aléatoire sera envoyé à votre contact"; +"A new random profile will be shared." = "Un nouveau profil aléatoire sera partagé."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Une connexion TCP distincte sera utilisée **pour chaque profil de chat que vous avez dans l'application**."; @@ -285,12 +303,12 @@ "Accept" = "Accepter"; /* No comment provided by engineer. */ -"Accept contact" = "Accepter le contact"; +"Accept connection request?" = "Accepter la demande de connexion ?"; /* notification body */ "Accept contact request from %@?" = "Accepter la demande de contact de %@ ?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Accepter en incognito"; /* call status */ @@ -333,7 +351,7 @@ "Advanced network settings" = "Paramètres réseau avancés"; /* chat item text */ -"agreeing encryption for %@…" = "acceptant le chiffrement pour %@…"; +"agreeing encryption for %@…" = "accord sur le chiffrement pour %@…"; /* chat item text */ "agreeing encryption…" = "accord sur le chiffrement…"; @@ -431,6 +449,9 @@ /* No comment provided by engineer. */ "App build: %@" = "Build de l'app : %@"; +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "L'application chiffre les nouveaux fichiers locaux (sauf les vidéos)."; + /* No comment provided by engineer. */ "App icon" = "Icône de l'app"; @@ -480,7 +501,7 @@ "Authentication unavailable" = "Authentification indisponible"; /* No comment provided by engineer. */ -"Auto-accept" = "Auto-acceptation"; +"Auto-accept" = "Auto-accepter"; /* No comment provided by engineer. */ "Auto-accept contact requests" = "Demandes de contact auto-acceptées"; @@ -524,6 +545,9 @@ /* 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."; +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgare, finnois, thaïlandais et ukrainien - grâce aux utilisateurs et à [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat) !"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -696,11 +720,17 @@ /* server test step */ "Connect" = "Se connecter"; +/* No comment provided by engineer. */ +"Connect directly" = "Se connecter directement"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Se connecter incognito"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "se connecter aux developpeurs de SimpleX Chat."; /* No comment provided by engineer. */ -"Connect via contact link?" = "Se connecter via le lien du contact ?"; +"Connect via contact link" = "Se connecter via un lien de contact"; /* No comment provided by engineer. */ "Connect via group link?" = "Se connecter via le lien du groupe ?"; @@ -712,7 +742,7 @@ "Connect via link / QR code" = "Se connecter via un lien / code QR"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Se connecter via un lien unique ?"; +"Connect via one-time link" = "Se connecter via un lien unique"; /* No comment provided by engineer. */ "connected" = "connecté"; @@ -756,9 +786,6 @@ /* chat list item title (it should not be shown */ "connection established" = "connexion établie"; -/* No comment provided by engineer. */ -"Connection request" = "Demande de connexion"; - /* No comment provided by engineer. */ "Connection request sent!" = "Demande de connexion envoyée !"; @@ -828,6 +855,9 @@ /* No comment provided by engineer. */ "Create link" = "Créer un lien"; +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Créer un nouveau profil sur [l'application de bureau](https://simplex.chat/downloads/). 💻"; + /* No comment provided by engineer. */ "Create one-time invitation link" = "Créer un lien d'invitation unique"; @@ -889,7 +919,7 @@ "Database ID: %d" = "ID de base de données : %d"; /* No comment provided by engineer. */ -"Database IDs and Transport isolation option." = "IDs de base de données et option d'isolation du transport."; +"Database IDs and Transport isolation option." = "IDs de base de données et option d'isolement du transport."; /* No comment provided by engineer. */ "Database is encrypted using a random passphrase, you can change it." = "La base de données est chiffrée à l'aide d'une phrase secrète aléatoire, que vous pouvez modifier."; @@ -1059,6 +1089,9 @@ /* rcv group event chat item */ "deleted group" = "groupe supprimé"; +/* No comment provided by engineer. */ +"Delivery" = "Distribution"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "Les accusés de réception sont désactivés !"; @@ -1087,7 +1120,7 @@ "different migration in the app/database: %@ / %@" = "migration différente dans l'app/la base de données : %@ / %@"; /* No comment provided by engineer. */ -"Different names, avatars and transport isolation." = "Différents noms, avatars et mode d'isolation de transport."; +"Different names, avatars and transport isolation." = "Différents noms, avatars et modes d'isolement de transport."; /* connection level description */ "direct" = "direct"; @@ -1107,6 +1140,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "Désactiver SimpleX Lock"; +/* No comment provided by engineer. */ +"disabled" = "désactivé"; + /* No comment provided by engineer. */ "Disappearing message" = "Message éphémère"; @@ -1128,6 +1164,9 @@ /* server test step */ "Disconnect" = "Se déconnecter"; +/* No comment provided by engineer. */ +"Discover and join groups" = "Découvrir et rejoindre des groupes"; + /* No comment provided by engineer. */ "Display name" = "Nom affiché"; @@ -1224,6 +1263,12 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Chiffrer la base de données ?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Chiffrer les fichiers locaux"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Chiffrement des fichiers et des médias stockés"; + /* No comment provided by engineer. */ "Encrypted database" = "Base de données chiffrée"; @@ -1252,7 +1297,7 @@ "encryption agreed for %@" = "chiffrement accepté pour %@"; /* chat item text */ -"encryption ok" = "chiffrement ok"; +"encryption ok" = "chiffrement OK"; /* chat item text */ "encryption ok for %@" = "chiffrement ok pour %@"; @@ -1335,6 +1380,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Erreur lors de la création du profil !"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Erreur lors du déchiffrement du fichier"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Erreur lors de la suppression de la base de données du chat"; @@ -1452,6 +1500,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; +/* No comment provided by engineer. */ +"event happened" = "event happened"; + /* No comment provided by engineer. */ "Exit without saving" = "Quitter sans sauvegarder"; @@ -1650,7 +1701,7 @@ /* No comment provided by engineer. */ "Hide:" = "Cacher :"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Historique"; /* time unit */ @@ -1719,7 +1770,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Configuration de serveur améliorée"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "En réponse à"; /* No comment provided by engineer. */ @@ -1729,10 +1780,7 @@ "Incognito mode" = "Mode Incognito"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "Le mode Incognito n'est pas supporté ici - votre profil principal sera envoyé aux membres du groupe"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact."; /* chat list item description */ "incognito via contact address link" = "mode incognito via le lien d'adresse du contact"; @@ -1797,6 +1845,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Adresse de serveur invalide !"; +/* item status text */ +"Invalid status" = "Statut invalide"; + /* No comment provided by engineer. */ "Invitation expired!" = "Invitation expirée !"; @@ -1986,7 +2037,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Erreur de distribution du message"; /* No comment provided by engineer. */ @@ -2058,6 +2109,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Plus d'améliorations à venir !"; +/* item status description */ +"Most likely this connection is deleted." = "Connexion probablement supprimée."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Il est fort probable que ce contact ait supprimé la connexion avec vous."; @@ -2094,6 +2148,9 @@ /* No comment provided by engineer. */ "New database archive" = "Nouvelle archive de base de données"; +/* No comment provided by engineer. */ +"New desktop app!" = "Nouvelle application de bureau !"; + /* No comment provided by engineer. */ "New display name" = "Nouveau nom d'affichage"; @@ -2130,6 +2187,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Aucun contact à ajouter"; +/* No comment provided by engineer. */ +"No delivery information" = "Pas d'information sur la distribution"; + /* No comment provided by engineer. */ "No device token!" = "Pas de token d'appareil !"; @@ -2302,8 +2362,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Coller le lien reçu"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact."; /* No comment provided by engineer. */ "peer-to-peer" = "pair-à-pair"; @@ -2461,6 +2521,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Plus d'informations sur notre GitHub."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Les accusés de réception sont désactivés"; + /* No comment provided by engineer. */ "received answer…" = "réponse reçu…"; @@ -2510,7 +2573,7 @@ "Reject" = "Rejeter"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Rejeter le contact (l'expéditeur N'en est PAS informé)"; +"Reject (sender NOT notified)" = "Rejeter le contact (l'expéditeur N'en est PAS informé)"; /* No comment provided by engineer. */ "Reject contact request" = "Rejeter la demande de contact"; @@ -2672,7 +2735,7 @@ "Scan server QR code" = "Scanner un code QR de serveur"; /* No comment provided by engineer. */ -"Search" = "Chercher"; +"Search" = "Recherche"; /* network option */ "sec" = "sec"; @@ -2764,9 +2827,15 @@ /* No comment provided by engineer. */ "Sending receipts is disabled for %lld contacts" = "L'envoi d'accusés de réception est désactivé pour %lld contacts"; +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "L'envoi de reçus est désactivé pour les groupes %lld"; + /* No comment provided by engineer. */ "Sending receipts is enabled for %lld contacts" = "L'envoi d'accusés de réception est activé pour %lld contacts"; +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "L'envoi de reçus est activé pour les groupes %lld"; + /* No comment provided by engineer. */ "Sending via" = "Envoi via"; @@ -2851,6 +2920,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Afficher les options pour les développeurs"; +/* No comment provided by engineer. */ +"Show last messages" = "Voir les derniers messages"; + /* No comment provided by engineer. */ "Show preview" = "Montrer l'aperçu"; @@ -2893,12 +2965,18 @@ /* simplex link type */ "SimpleX one-time invitation" = "Invitation unique SimpleX"; +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Mode incognito simplifié"; + /* No comment provided by engineer. */ "Skip" = "Passer"; /* No comment provided by engineer. */ "Skipped messages" = "Messages manqués"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Petits groupes (max 20)"; + /* No comment provided by engineer. */ "SMP servers" = "Serveurs SMP"; @@ -2972,7 +3050,7 @@ "Tap button " = "Appuyez sur le bouton "; /* No comment provided by engineer. */ -"Tap to activate profile." = "Appuyez pour activer le profil."; +"Tap to activate profile." = "Appuyez pour activer un profil."; /* No comment provided by engineer. */ "Tap to join" = "Appuyez pour rejoindre"; @@ -3083,7 +3161,7 @@ "These settings are for your current profile **%@**." = "Ces paramètres s'appliquent à votre profil actuel **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Ils peuvent être remplacés dans les paramètres des contacts"; +"They can be overridden in contact and group settings." = "Ils peuvent être modifiés dans les paramètres des contacts et des groupes."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Cette action ne peut être annulée - tous les fichiers et médias reçus et envoyés seront supprimés. Les photos à faible résolution seront conservées."; @@ -3097,6 +3175,9 @@ /* notification title */ "this contact" = "ce contact"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Ce groupe compte plus de %lld membres, les accusés de réception ne sont pas envoyés."; + /* No comment provided by engineer. */ "This group no longer exists." = "Ce groupe n'existe plus."; @@ -3109,9 +3190,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Pour se connecter, votre contact peut scanner le code QR ou utiliser le lien dans l'application."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Pour trouver le profil utilisé lors d'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat."; - /* No comment provided by engineer. */ "To make a new connection" = "Pour établir une nouvelle connexion"; @@ -3137,7 +3215,10 @@ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils."; /* No comment provided by engineer. */ -"Transport isolation" = "Isolement du transport"; +"Toggle incognito when connecting." = "Basculer en mode incognito lors de la connexion."; + +/* No comment provided by engineer. */ +"Transport isolation" = "Transport isolé"; /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@)."; @@ -3157,7 +3238,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Impossible d'enregistrer un message vocal"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Erreur inattendue : %@"; /* No comment provided by engineer. */ @@ -3221,7 +3302,7 @@ "Update network settings?" = "Mettre à jour les paramètres réseau ?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Mettre à jour le mode d'isolation du transport ?"; +"Update transport isolation mode?" = "Mettre à jour le mode d'isolement du transport ?"; /* rcv group event chat item */ "updated group profile" = "mise à jour du profil de groupe"; @@ -3244,12 +3325,18 @@ /* No comment provided by engineer. */ "Use chat" = "Utiliser le chat"; +/* No comment provided by engineer. */ +"Use current profile" = "Utiliser le profil actuel"; + /* No comment provided by engineer. */ "Use for new connections" = "Utiliser pour les nouvelles connexions"; /* No comment provided by engineer. */ "Use iOS call interface" = "Utiliser l'interface d'appel d'iOS"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Utiliser un nouveau profil incognito"; + /* No comment provided by engineer. */ "Use server" = "Utiliser ce serveur"; @@ -3479,7 +3566,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil."; /* No comment provided by engineer. */ -"You invited your contact" = "Vous avez invité votre contact"; +"You invited a contact" = "Vous avez invité votre contact"; /* No comment provided by engineer. */ "You joined this group" = "Vous avez rejoint ce groupe"; @@ -3559,9 +3646,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Votre profil de chat sera envoyé aux membres du groupe"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Votre profil de chat sera envoyé à votre contact"; - /* No comment provided by engineer. */ "Your chat profiles" = "Vos profils de chat"; @@ -3596,10 +3680,10 @@ "Your privacy" = "Votre vie privée"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts.\nLes serveurs SimpleX ne peuvent pas voir votre profil."; +"Your profile **%@** will be shared." = "Votre profil **%@** sera partagé."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Votre profil sera envoyé au contact qui vous a envoyé ce lien"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Votre profil est stocké sur votre appareil et est seulement partagé avec vos contacts.\nLes serveurs SimpleX ne peuvent pas voir votre profil."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Votre profil, vos contacts et les messages reçus sont stockés sur votre appareil."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 915cccf871..a9a663dfdf 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_corsivo_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- connessione al [servizio directory](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- ricevute di consegna (fino a 20 membri).\n- più veloce e più stabile."; + /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- recapito dei messaggi più stabile.\n- gruppi un po' migliorati.\n- e altro ancora!"; @@ -88,6 +91,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*grassetto*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Cronologia"; + +/* copied message info */ +"## In reply to" = "## In risposta a"; + /* No comment provided by engineer. */ "#secret#" = "#segreto#"; @@ -106,6 +118,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ e %@ sono connessi/e"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ alle %2$@:"; @@ -124,6 +139,9 @@ /* notification title */ "%@ wants to connect!" = "%@ si vuole connettere!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ e altri %lld membri sono connessi"; + /* copied message info */ "%@:" = "%@:"; @@ -166,6 +184,9 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld minuti"; +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld nuove lingue dell'interfaccia"; + /* No comment provided by engineer. */ "%lld second(s)" = "%lld secondo/i"; @@ -245,10 +266,7 @@ "A new contact" = "Un contatto nuovo"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Verrà inviato un profilo casuale al contatto da cui hai ricevuto questo link"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Verrà inviato un profilo casuale al tuo contatto"; +"A new random profile will be shared." = "Verrà condiviso un nuovo profilo casuale."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Verrà usata una connessione TCP separata **per ogni profilo di chat presente nell'app**."; @@ -285,12 +303,12 @@ "Accept" = "Accetta"; /* No comment provided by engineer. */ -"Accept contact" = "Accetta il contatto"; +"Accept connection request?" = "Accettare la richiesta di connessione?"; /* notification body */ "Accept contact request from %@?" = "Accettare la richiesta di contatto da %@?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Accetta in incognito"; /* call status */ @@ -431,6 +449,9 @@ /* No comment provided by engineer. */ "App build: %@" = "Build dell'app: %@"; +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "L'app cripta i nuovi file locali (eccetto i video)."; + /* No comment provided by engineer. */ "App icon" = "Icona app"; @@ -524,6 +545,9 @@ /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Sia tu che il tuo contatto potete inviare messaggi vocali."; +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgaro, finlandese, tailandese e ucraino - grazie agli utenti e a [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -696,11 +720,17 @@ /* server test step */ "Connect" = "Connetti"; +/* No comment provided by engineer. */ +"Connect directly" = "Connetti direttamente"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Connetti in incognito"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "connettiti agli sviluppatori di SimpleX Chat."; /* No comment provided by engineer. */ -"Connect via contact link?" = "Connettere via link del contatto?"; +"Connect via contact link" = "Connetti via link del contatto"; /* No comment provided by engineer. */ "Connect via group link?" = "Connettere via link del gruppo?"; @@ -712,7 +742,7 @@ "Connect via link / QR code" = "Connetti via link / codice QR"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Connettere via link una tantum?"; +"Connect via one-time link" = "Connetti via link una tantum"; /* No comment provided by engineer. */ "connected" = "connesso/a"; @@ -756,9 +786,6 @@ /* chat list item title (it should not be shown */ "connection established" = "connessione stabilita"; -/* No comment provided by engineer. */ -"Connection request" = "Richiesta di connessione"; - /* No comment provided by engineer. */ "Connection request sent!" = "Richiesta di connessione inviata!"; @@ -828,6 +855,9 @@ /* No comment provided by engineer. */ "Create link" = "Crea link"; +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Crea un nuovo profilo nell'[app desktop](https://simplex.chat/downloads/). 💻"; + /* No comment provided by engineer. */ "Create one-time invitation link" = "Crea link di invito una tantum"; @@ -1059,6 +1089,9 @@ /* rcv group event chat item */ "deleted group" = "gruppo eliminato"; +/* No comment provided by engineer. */ +"Delivery" = "Consegna"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "Le ricevute di consegna sono disattivate!"; @@ -1107,6 +1140,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "Disattiva SimpleX Lock"; +/* No comment provided by engineer. */ +"disabled" = "disattivato"; + /* No comment provided by engineer. */ "Disappearing message" = "Messaggio a tempo"; @@ -1128,6 +1164,9 @@ /* server test step */ "Disconnect" = "Disconnetti"; +/* No comment provided by engineer. */ +"Discover and join groups" = "Scopri ed unisciti ai gruppi"; + /* No comment provided by engineer. */ "Display name" = "Nome da mostrare"; @@ -1224,6 +1263,12 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Crittografare il database?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Cripta i file locali"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Crittografia di file e media memorizzati"; + /* No comment provided by engineer. */ "Encrypted database" = "Database crittografato"; @@ -1335,6 +1380,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Errore nella creazione del profilo!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Errore decifrando il file"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Errore nell'eliminazione del database della chat"; @@ -1452,6 +1500,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; +/* No comment provided by engineer. */ +"event happened" = "evento accaduto"; + /* No comment provided by engineer. */ "Exit without saving" = "Esci senza salvare"; @@ -1650,7 +1701,7 @@ /* No comment provided by engineer. */ "Hide:" = "Nascondi:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Cronologia"; /* time unit */ @@ -1719,7 +1770,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Configurazione del server migliorata"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "In risposta a"; /* No comment provided by engineer. */ @@ -1729,10 +1780,7 @@ "Incognito mode" = "Modalità incognito"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "La modalità in incognito non è supportata qui: il tuo profilo principale verrà inviato ai membri del gruppo"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "La modalità in incognito protegge la privacy del nome e dell'immagine del tuo profilo principale: per ogni nuovo contatto viene creato un nuovo profilo casuale."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "La modalità in incognito protegge la tua privacy usando un nuovo profilo casuale per ogni contatto."; /* chat list item description */ "incognito via contact address link" = "incognito via link indirizzo del contatto"; @@ -1797,6 +1845,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Indirizzo del server non valido!"; +/* item status text */ +"Invalid status" = "Stato non valido"; + /* No comment provided by engineer. */ "Invitation expired!" = "Invito scaduto!"; @@ -1975,7 +2026,7 @@ "Member" = "Membro"; /* rcv group event chat item */ -"member connected" = "connesso/a"; +"member connected" = "è connesso/a"; /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Tutti i membri del gruppo verranno avvisati."; @@ -1986,7 +2037,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Errore di recapito del messaggio"; /* No comment provided by engineer. */ @@ -2058,6 +2109,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Altri miglioramenti sono in arrivo!"; +/* item status description */ +"Most likely this connection is deleted." = "Probabilmente questa connessione è stata eliminata."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Probabilmente questo contatto ha eliminato la connessione con te."; @@ -2094,6 +2148,9 @@ /* No comment provided by engineer. */ "New database archive" = "Nuovo archivio database"; +/* No comment provided by engineer. */ +"New desktop app!" = "Nuova app desktop!"; + /* No comment provided by engineer. */ "New display name" = "Nuovo nome da mostrare"; @@ -2130,6 +2187,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Nessun contatto da aggiungere"; +/* No comment provided by engineer. */ +"No delivery information" = "Nessuna informazione sulla consegna"; + /* No comment provided by engineer. */ "No device token!" = "Nessun token del dispositivo!"; @@ -2302,8 +2362,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Incolla il link ricevuto"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto."; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; @@ -2461,6 +2521,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Maggiori informazioni nel nostro repository GitHub."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Le ricevute sono disattivate"; + /* No comment provided by engineer. */ "received answer…" = "risposta ricevuta…"; @@ -2510,7 +2573,7 @@ "Reject" = "Rifiuta"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Rifiuta contatto (mittente NON avvisato)"; +"Reject (sender NOT notified)" = "Rifiuta contatto (mittente NON avvisato)"; /* No comment provided by engineer. */ "Reject contact request" = "Rifiuta la richiesta di contatto"; @@ -2764,9 +2827,15 @@ /* No comment provided by engineer. */ "Sending receipts is disabled for %lld contacts" = "L'invio di ricevute è disattivato per %lld contatti"; +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "L'invio di ricevute è disattivato per %lld gruppi"; + /* No comment provided by engineer. */ "Sending receipts is enabled for %lld contacts" = "L'invio di ricevute è attivo per %lld contatti"; +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "L'invio di ricevute è attivo per %lld gruppi"; + /* No comment provided by engineer. */ "Sending via" = "Invio tramite"; @@ -2851,6 +2920,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Mostra opzioni sviluppatore"; +/* No comment provided by engineer. */ +"Show last messages" = "Mostra ultimi messaggi"; + /* No comment provided by engineer. */ "Show preview" = "Mostra anteprima"; @@ -2893,12 +2965,18 @@ /* simplex link type */ "SimpleX one-time invitation" = "Invito SimpleX una tantum"; +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Modalità incognito semplificata"; + /* No comment provided by engineer. */ "Skip" = "Salta"; /* No comment provided by engineer. */ "Skipped messages" = "Messaggi saltati"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Piccoli gruppi (max 20)"; + /* No comment provided by engineer. */ "SMP servers" = "Server SMP"; @@ -3083,7 +3161,7 @@ "These settings are for your current profile **%@**." = "Queste impostazioni sono per il tuo profilo attuale **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Possono essere sovrascritte nelle impostazioni dei contatti"; +"They can be overridden in contact and group settings." = "Possono essere sovrascritte nelle impostazioni dei contatti e dei gruppi."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Questa azione non può essere annullata: tutti i file e i media ricevuti e inviati verranno eliminati. Rimarranno le immagini a bassa risoluzione."; @@ -3097,6 +3175,9 @@ /* notification title */ "this contact" = "questo contatto"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Questo gruppo ha più di %lld membri, le ricevute di consegna non vengono inviate."; + /* No comment provided by engineer. */ "This group no longer exists." = "Questo gruppo non esiste più."; @@ -3109,9 +3190,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell'app."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Per trovare il profilo usato per una connessione in incognito, tocca il nome del contatto o del gruppo in cima alla chat."; - /* No comment provided by engineer. */ "To make a new connection" = "Per creare una nuova connessione"; @@ -3136,6 +3214,9 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi."; +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Attiva/disattiva l'incognito quando ti colleghi."; + /* No comment provided by engineer. */ "Transport isolation" = "Isolamento del trasporto"; @@ -3157,7 +3238,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Impossibile registrare il messaggio vocale"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Errore imprevisto: % @"; /* No comment provided by engineer. */ @@ -3244,12 +3325,18 @@ /* No comment provided by engineer. */ "Use chat" = "Usa la chat"; +/* No comment provided by engineer. */ +"Use current profile" = "Usa il profilo attuale"; + /* No comment provided by engineer. */ "Use for new connections" = "Usa per connessioni nuove"; /* No comment provided by engineer. */ "Use iOS call interface" = "Usa interfaccia di chiamata iOS"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Usa nuovo profilo in incognito"; + /* No comment provided by engineer. */ "Use server" = "Usa il server"; @@ -3479,7 +3566,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Devi inserire la password ogni volta che si avvia l'app: non viene memorizzata sul dispositivo."; /* No comment provided by engineer. */ -"You invited your contact" = "Hai invitato il contatto"; +"You invited a contact" = "Hai invitato il contatto"; /* No comment provided by engineer. */ "You joined this group" = "Sei entrato/a in questo gruppo"; @@ -3559,9 +3646,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Il tuo profilo di chat verrà inviato ai membri del gruppo"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Il tuo profilo di chat verrà inviato al tuo contatto"; - /* No comment provided by engineer. */ "Your chat profiles" = "I tuoi profili di chat"; @@ -3596,10 +3680,10 @@ "Your privacy" = "La tua privacy"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.\nI server di SimpleX non possono vedere il tuo profilo."; +"Your profile **%@** will be shared." = "Il tuo profilo **%@** verrà condiviso."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Il tuo profilo verrà inviato al contatto da cui hai ricevuto questo link"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti.\nI server di SimpleX non possono vedere il tuo profilo."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 5e02f6f8a4..6fadf87590 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_斜体_"; +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- より安定したメッセージ配信。\n- 改良されたグループ。\n- などなど!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 最長 5 分間の音声メッセージ。\n- 消えるまでのカスタム時間。\n- 編集履歴。"; @@ -85,6 +88,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*太文字*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## 履歴"; + +/* copied message info */ +"## In reply to" = "## 返信先"; + /* No comment provided by engineer. */ "#secret#" = "シークレット"; @@ -103,6 +115,12 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ と %@ は接続中"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ at %2$@:"; + /* notification title */ "%@ is connected!" = "%@ 接続中!"; @@ -118,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "%@ が接続を希望しています!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ および %lld 人のメンバーが接続中"; + /* copied message info */ "%@:" = "%@:"; @@ -232,14 +253,14 @@ /* No comment provided by engineer. */ "30 seconds" = "30秒"; +/* No comment provided by engineer. */ +"A few more things" = "その他"; + /* notification title */ "A new contact" = "新しい連絡先"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "このリンクの送信元にランダムなプロフィール(ダミー)が送られます"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "連絡先にランダムなプロフィール(ダミー)が送られます"; +"A new random profile will be shared." = "新しいランダムなプロファイルが共有されます。"; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "**アプリ内のチャット プロフィールごとに**、個別の TCP 接続が使用されます。"; @@ -247,6 +268,15 @@ /* No comment provided by engineer. */ "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**アプリ内のチャット プロファイルごとに**、個別の TCP 接続が使用されます。\n**注意**:多くの接続がある場合、バッテリーと通信量の消費が大幅に増加し、一部の接続に失敗することがあります。"; +/* No comment provided by engineer. */ +"Abort" = "中止"; + +/* No comment provided by engineer. */ +"Abort changing address" = "アドレス変更の中止"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "アドレス変更を中止しますか?"; + /* No comment provided by engineer. */ "About SimpleX" = "SimpleXについて"; @@ -267,12 +297,12 @@ "Accept" = "承諾"; /* No comment provided by engineer. */ -"Accept contact" = "連絡を受け入れる"; +"Accept connection request?" = "連絡を受け入れる"; /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "シークレットモードで承諾"; /* call status */ @@ -302,6 +332,9 @@ /* No comment provided by engineer. */ "Address" = "アドレス"; +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "アドレス変更は中止されます。古い受信アドレスが使用されます。"; + /* member role */ "admin" = "管理者"; @@ -311,6 +344,12 @@ /* No comment provided by engineer. */ "Advanced network settings" = "ネットワーク詳細設定"; +/* chat item text */ +"agreeing encryption for %@…" = "%@の暗号化に同意しています…"; + +/* chat item text */ +"agreeing encryption…" = "暗号化に同意しています…"; + /* No comment provided by engineer. */ "All app data is deleted." = "すべてのアプリデータが削除されます。"; @@ -359,6 +398,9 @@ /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages." = "送信済みメッセージの永久削除を許可する。"; +/* No comment provided by engineer. */ +"Allow to send files and media." = "ファイルやメディアの送信を許可する。"; + /* No comment provided by engineer. */ "Allow to send voice messages." = "音声メッセージの送信を許可する。"; @@ -473,6 +515,9 @@ /* No comment provided by engineer. */ "Bad message ID" = "メッセージ ID が正しくありません"; +/* No comment provided by engineer. */ +"Better messages" = "より良いメッセージ"; + /* No comment provided by engineer. */ "bold" = "太文字"; @@ -570,6 +615,12 @@ /* rcv group event chat item */ "changed your role to %@" = "あなたの役割を %@ に変更しました"; +/* chat item text */ +"changing address for %@…" = "%@ のアドレスを変更しています…"; + +/* chat item text */ +"changing address…" = "アドレスを変更しています…"; + /* No comment provided by engineer. */ "Chat archive" = "チャットのアーカイブ"; @@ -657,11 +708,17 @@ /* server test step */ "Connect" = "接続"; +/* No comment provided by engineer. */ +"Connect directly" = "直接接続する"; + +/* No comment provided by engineer. */ +"Connect incognito" = "シークレットモードで接続"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "SimpleX Chat 開発者に接続します。"; /* No comment provided by engineer. */ -"Connect via contact link?" = "連絡先リンク経由で接続しますか?"; +"Connect via contact link" = "連絡先リンク経由で接続しますか?"; /* No comment provided by engineer. */ "Connect via group link?" = "グループリンク経由で接続しますか?"; @@ -673,7 +730,7 @@ "Connect via link / QR code" = "リンク・QRコード経由で接続"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "使い捨てリンク経由で接続しますか?"; +"Connect via one-time link" = "使い捨てリンク経由で接続しますか?"; /* No comment provided by engineer. */ "connected" = "接続中"; @@ -717,9 +774,6 @@ /* chat list item title (it should not be shown */ "connection established" = "接続済み"; -/* No comment provided by engineer. */ -"Connection request" = "接続のリクエスト"; - /* No comment provided by engineer. */ "Connection request sent!" = "接続リクエストを送信しました!"; @@ -759,6 +813,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "連絡先の設定"; +/* No comment provided by engineer. */ +"Contacts" = "連絡先"; + /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "連絡先はメッセージを削除対象とすることができます。あなたには閲覧可能です。"; @@ -894,6 +951,12 @@ /* pref value */ "default (%@)" = "デフォルト (%@)"; +/* No comment provided by engineer. */ +"default (no)" = "デフォルト(いいえ)"; + +/* No comment provided by engineer. */ +"default (yes)" = "デフォルト(はい)"; + /* chat item action */ "Delete" = "削除"; @@ -1011,6 +1074,12 @@ /* rcv group event chat item */ "deleted group" = "削除されたグループ"; +/* No comment provided by engineer. */ +"Delivery" = "Delivery"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Delivery receipts are disabled!"; + /* No comment provided by engineer. */ "Description" = "説明"; @@ -1044,9 +1113,18 @@ /* No comment provided by engineer. */ "Direct messages between members are prohibited in this group." = "このグループではメンバー間のダイレクトメッセージが使用禁止です。"; +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "無効にする(設定の優先を維持)"; + +/* No comment provided by engineer. */ +"Disable for all" = "すべて無効"; + /* authentication reason */ "Disable SimpleX Lock" = "SimpleXロックを無効にする"; +/* No comment provided by engineer. */ +"disabled" = "無効"; + /* No comment provided by engineer. */ "Disappearing message" = "消えるメッセージ"; @@ -1083,6 +1161,9 @@ /* No comment provided by engineer. */ "Don't create address" = "アドレスを作成しないでください"; +/* No comment provided by engineer. */ +"Don't enable" = "有効にしない"; + /* No comment provided by engineer. */ "Don't show again" = "次から表示しない"; @@ -1113,9 +1194,15 @@ /* No comment provided by engineer. */ "Enable" = "有効"; +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "有効にする(設定の優先を維持)"; + /* No comment provided by engineer. */ "Enable automatic message deletion?" = "自動メッセージ削除を有効にしますか?"; +/* No comment provided by engineer. */ +"Enable for all" = "すべて有効"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "即時通知を有効にしますか?"; @@ -1155,6 +1242,9 @@ /* No comment provided by engineer. */ "Encrypt database?" = "データベースを暗号化しますか?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "ローカルファイルを暗号化する"; + /* No comment provided by engineer. */ "Encrypted database" = "暗号化済みデータベース"; @@ -1176,6 +1266,30 @@ /* notification */ "Encrypted message: unexpected error" = "暗号化されたメッセージ : 予期しないエラー"; +/* chat item text */ +"encryption agreed" = "暗号化に同意しました"; + +/* chat item text */ +"encryption agreed for %@" = "%@ の暗号化に同意しました"; + +/* chat item text */ +"encryption ok" = "暗号化OK"; + +/* chat item text */ +"encryption ok for %@" = "%@ の暗号化OK"; + +/* chat item text */ +"encryption re-negotiation allowed" = "暗号化の再ネゴシエーションを許可"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "%@ の暗号化の再ネゴシエーションを許可"; + +/* chat item text */ +"encryption re-negotiation required" = "暗号化の再ネゴシエーションが必要"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "%@ の暗号化の再ネゴシエーションが必要"; + /* No comment provided by engineer. */ "ended" = "終了"; @@ -1209,6 +1323,9 @@ /* No comment provided by engineer. */ "Error" = "エラー"; +/* No comment provided by engineer. */ +"Error aborting address change" = "アドレス変更中止エラー"; + /* No comment provided by engineer. */ "Error accepting contact request" = "連絡先リクエストの承諾にエラー発生"; @@ -1239,6 +1356,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "プロフィール作成にエラー発生!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "ファイルの復号エラー"; + /* No comment provided by engineer. */ "Error deleting chat database" = "チャットデータベース削除にエラー発生"; @@ -1320,6 +1440,9 @@ /* No comment provided by engineer. */ "Error switching profile!" = "プロフィール切り替えにエラー発生!"; +/* No comment provided by engineer. */ +"Error synchronizing connection" = "接続の同期エラー"; + /* No comment provided by engineer. */ "Error updating group link" = "グループのリンクのアップデートにエラー発生"; @@ -1344,6 +1467,12 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "エラー: 無効なURL"; +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "会話中に無効になっている場合でも。"; + +/* No comment provided by engineer. */ +"event happened" = "イベント発生"; + /* No comment provided by engineer. */ "Exit without saving" = "保存せずに閉じる"; @@ -1365,6 +1494,9 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "送信者がオンラインになるまでの待ち時間がなく、速い!"; +/* No comment provided by engineer. */ +"Favorite" = "お気に入り"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "ファイルはサーバーから削除されます。"; @@ -1380,6 +1512,42 @@ /* No comment provided by engineer. */ "Files & media" = "ファイルとメディア"; +/* chat feature */ +"Files and media" = "ファイルとメディア"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "このグループでは、ファイルとメディアは禁止されています。"; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "ファイルとメディアは禁止されています!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "未読とお気に入りをフィルターします。"; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "ついに、私たちはそれらを手に入れました! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "チャットを素早く検索"; + +/* No comment provided by engineer. */ +"Fix" = "修正"; + +/* No comment provided by engineer. */ +"Fix connection" = "接続を修正"; + +/* No comment provided by engineer. */ +"Fix connection?" = "接続を修正しますか?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "バックアップの復元後に暗号化を修正します。"; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "連絡先による修正はサポートされていません"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "グループメンバーによる修正はサポートされていません"; + /* No comment provided by engineer. */ "For console" = "コンソール"; @@ -1446,6 +1614,9 @@ /* No comment provided by engineer. */ "Group members can send disappearing messages." = "グループのメンバーが消えるメッセージを送信できます。"; +/* No comment provided by engineer. */ +"Group members can send files and media." = "グループメンバーはファイルやメディアを送信できます。"; + /* No comment provided by engineer. */ "Group members can send voice messages." = "グループのメンバーが音声メッセージを送信できます。"; @@ -1500,7 +1671,7 @@ /* No comment provided by engineer. */ "Hide:" = "隠す:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "履歴"; /* time unit */ @@ -1569,6 +1740,9 @@ /* No comment provided by engineer. */ "Improved server configuration" = "サーバ設定の向上"; +/* No comment provided by engineer. */ +"In reply to" = "返信先"; + /* No comment provided by engineer. */ "Incognito" = "シークレットモード"; @@ -1576,10 +1750,7 @@ "Incognito mode" = "シークレットモード"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "ここではシークレットモードが無効です。メインのプロフィールがグループのメンバーに送られます"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されます。"; +"Incognito mode protects your privacy by using a new random profile for each contact." = "シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。"; /* chat list item description */ "incognito via contact address link" = "連絡先リンク経由でシークレットモード"; @@ -1644,6 +1815,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "無効なサーバアドレス!"; +/* item status text */ +"Invalid status" = "無効なステータス"; + /* No comment provided by engineer. */ "Invitation expired!" = "招待が期限切れました!"; @@ -1722,6 +1896,9 @@ /* No comment provided by engineer. */ "Joining group" = "グループに参加"; +/* No comment provided by engineer. */ +"Keep your connections" = "接続を維持"; + /* No comment provided by engineer. */ "Keychain error" = "キーチェーンのエラー"; @@ -1779,6 +1956,9 @@ /* No comment provided by engineer. */ "Make a private connection" = "プライベートな接続をする"; +/* No comment provided by engineer. */ +"Make one message disappear" = "メッセージを1つ消す"; + /* No comment provided by engineer. */ "Make profile private!" = "プロフィールを非表示にできます!"; @@ -1827,7 +2007,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "メンバーをグループから除名する (※元に戻せません※)!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "メッセージ送信エラー"; /* No comment provided by engineer. */ @@ -1896,6 +2076,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "まだまだ改善してまいります!"; +/* item status description */ +"Most likely this connection is deleted." = "おそらく、この接続は削除されています。"; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "恐らくこの連絡先があなたとの接続を削除しました。"; @@ -1968,21 +2151,33 @@ /* No comment provided by engineer. */ "No contacts to add" = "追加できる連絡先がありません"; +/* No comment provided by engineer. */ +"No delivery information" = "送信情報なし"; + /* No comment provided by engineer. */ "No device token!" = "デバイストークンがありません!"; /* No comment provided by engineer. */ "no e2e encryption" = "エンドツーエンド暗号化がありません"; +/* No comment provided by engineer. */ +"No filtered chats" = "フィルタされたチャットはありません"; + /* No comment provided by engineer. */ "No group!" = "グループが見つかりません!"; +/* No comment provided by engineer. */ +"No history" = "履歴はありません"; + /* No comment provided by engineer. */ "No permission to record voice message" = "音声メッセージを録音する権限がありません"; /* No comment provided by engineer. */ "No received or sent files" = "送受信済みのファイルがありません"; +/* copied message info in history */ +"no text" = "テキストなし"; + /* No comment provided by engineer. */ "Notifications" = "通知"; @@ -2041,6 +2236,9 @@ /* No comment provided by engineer. */ "Only group owners can change group preferences." = "グループ設定を変えられるのはグループのオーナーだけです。"; +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "ファイルやメディアを有効にできるのは、グループオーナーだけです。"; + /* No comment provided by engineer. */ "Only group owners can enable voice messages." = "音声メッセージを利用可能に設定できるのはグループのオーナーだけです。"; @@ -2128,8 +2326,8 @@ /* No comment provided by engineer. */ "Paste received link" = "頂いたリンクを貼り付ける"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。"; +/* placeholder */ +"Paste the link you received to connect with your contact." = "連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。"; /* No comment provided by engineer. */ "peer-to-peer" = "P2P"; @@ -2242,6 +2440,9 @@ /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "消えるメッセージを使用禁止にする。"; +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "ファイルやメディアの送信を禁止します。"; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "音声メッセージを使用禁止にする。"; @@ -2254,12 +2455,18 @@ /* No comment provided by engineer. */ "Protocol timeout" = "プロトコル・タイムアウト"; +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "KB あたりのプロトコル タイムアウト"; + /* No comment provided by engineer. */ "Push notifications" = "プッシュ通知"; /* No comment provided by engineer. */ "Rate the app" = "アプリを評価"; +/* chat item menu */ +"React…" = "反応する…"; + /* No comment provided by engineer. */ "Read" = "読む"; @@ -2296,6 +2503,9 @@ /* message info title */ "Received message" = "受信したメッセージ"; +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。"; + /* No comment provided by engineer. */ "Receiving file will be stopped." = "ファイルの受信を停止します。"; @@ -2305,6 +2515,12 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "受信者には、入力時に更新内容が表示されます。"; +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "接続されているすべてのサーバーを再接続して、メッセージを強制的に配信します。 追加のトラフィックを使用します。"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "サーバーに再接続しますか?"; + /* No comment provided by engineer. */ "Record updated at" = "レコード更新日時"; @@ -2318,7 +2534,7 @@ "Reject" = "拒否"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "連絡を拒否(送信者には通知されません)"; +"Reject (sender NOT notified)" = "連絡を拒否(送信者には通知されません)"; /* No comment provided by engineer. */ "Reject contact request" = "連絡要求を拒否する"; @@ -2353,6 +2569,15 @@ /* rcv group event chat item */ "removed you" = "あなたを除名しました"; +/* No comment provided by engineer. */ +"Renegotiate" = "再ネゴシエート"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "暗号化の再ネゴシエート"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "暗号化を再ネゴシエートしますか?"; + /* chat item action */ "Reply" = "返信"; @@ -2491,6 +2716,9 @@ /* No comment provided by engineer. */ "Security code" = "セキュリティコード"; +/* chat item text */ +"security code changed" = "セキュリティコードが変更されました"; + /* No comment provided by engineer. */ "Select" = "選択"; @@ -2629,6 +2857,9 @@ /* No comment provided by engineer. */ "Show developer options" = "開発者向けオプションを表示"; +/* No comment provided by engineer. */ +"Show last messages" = "最新のメッセージを表示"; + /* No comment provided by engineer. */ "Show preview" = "プレビューを表示"; @@ -2677,9 +2908,15 @@ /* No comment provided by engineer. */ "Skipped messages" = "飛ばしたメッセージ"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "小グループ(最大20名)"; + /* No comment provided by engineer. */ "SMP servers" = "SMPサーバ"; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。"; + /* notification title */ "Somebody" = "誰か"; @@ -2809,6 +3046,9 @@ /* No comment provided by engineer. */ "The created archive is available via app Settings / Database / Old database archive." = "作成されたアーカイブは、アプリの設定/データベース/過去のデータベースアーカイブから利用できます。"; +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!"; + /* No comment provided by engineer. */ "The group is fully decentralized – it is visible only to the members." = "グループは完全分散型で、メンバーしか内容を見れません。"; @@ -2833,6 +3073,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "プロフィールは連絡先にしか共有されません。"; +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "長らくお待たせしました! ✅"; + /* No comment provided by engineer. */ "The sender will NOT be notified" = "送信者には通知されません"; @@ -2848,6 +3091,12 @@ /* No comment provided by engineer. */ "There should be at least one visible user profile." = "少なくとも1つのユーザープロフィールが表示されている必要があります。"; +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "これらの設定は現在のプロファイル **%@** 用です。"; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "これらは連絡先の設定が優先します。"; + /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "ファイルとメディアが全て削除されます (※元に戻せません※)。低解像度の画像が残ります。"; @@ -2872,9 +3121,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "接続するにはQRコードを読み込むか、アプリ内のリンクを使用する必要があります。"; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "シークレットモード接続のプロフィールを確認するには、チャットの上部の連絡先、またはグループ名をタップします。"; - /* No comment provided by engineer. */ "To make a new connection" = "新規に接続する場合"; @@ -2920,12 +3166,15 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "音声メッセージを録音できません"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "予期しないエラー: %@"; /* No comment provided by engineer. */ "Unexpected migration state" = "予期しない移行状態"; +/* No comment provided by engineer. */ +"Unfav." = "お気に入りを取り消す。"; + /* No comment provided by engineer. */ "Unhide" = "表示にする"; @@ -3004,12 +3253,18 @@ /* No comment provided by engineer. */ "Use chat" = "チャット"; +/* No comment provided by engineer. */ +"Use current profile" = "現在のプロファイルを使用する"; + /* No comment provided by engineer. */ "Use for new connections" = "新しい接続に使う"; /* No comment provided by engineer. */ "Use iOS call interface" = "iOS通話インターフェースを使用する"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "新しいシークレットプロファイルを使用する"; + /* No comment provided by engineer. */ "Use server" = "サーバを使う"; @@ -3178,6 +3433,12 @@ /* No comment provided by engineer. */ "You can create it later" = "後からでも作成できます"; +/* No comment provided by engineer. */ +"You can enable later via Settings" = "あとで設定から有効にできます"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。"; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。"; @@ -3233,7 +3494,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。"; /* No comment provided by engineer. */ -"You invited your contact" = "連絡先に招待を送りました"; +"You invited a contact" = "連絡先に招待を送りました"; /* No comment provided by engineer. */ "You joined this group" = "グループに参加しました"; @@ -3313,9 +3574,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "あなたのチャットプロフィールが他のグループメンバーに送られます"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "あなたのチャットプロフィールが連絡相手に送られます"; - /* No comment provided by engineer. */ "Your chat profiles" = "あなたのチャットプロフィール"; @@ -3350,10 +3608,10 @@ "Your privacy" = "あなたのプライバシー"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。\nSimpleX サーバーはあなたのプロファイルを参照できません。"; +"Your profile **%@** will be shared." = "あなたのプロファイル **%@** が共有されます。"; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "あなたのプロフィールは、このリンクを受け取った連絡先に送信されます"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "プロフィールはデバイスに保存され、連絡先とのみ共有されます。\nSimpleX サーバーはあなたのプロファイルを参照できません。"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "あなたのプロフィール、連絡先、送信したメッセージがご自分の端末に保存されます。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 1386b8cd23..a218eeeff3 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_cursief_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- verbinding maken met [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! \n- ontvangst bevestiging(tot 20 leden). \n- sneller en stabieler."; + /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- stabielere berichtbezorging.\n- een beetje betere groepen.\n- en meer!"; @@ -56,7 +59,7 @@ "**Add new contact**: to create your one-time QR Code for your contact." = "**Nieuw contact toevoegen**: om uw eenmalige QR-code of link voor uw contact te maken."; /* No comment provided by engineer. */ -"**Create link / QR code** for your contact to use." = "**Maak een link / QR-code aan** die uw contactpersoon kan gebruiken."; +"**Create link / QR code** for your contact to use." = "**Maak een link / QR-code aan** die uw contact kan gebruiken."; /* No comment provided by engineer. */ "**e2e encrypted** audio call" = "**e2e versleuteld** audio gesprek"; @@ -80,7 +83,7 @@ "**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Aanbevolen**: apparaattoken en meldingen worden naar de SimpleX Chat-meldingsserver gestuurd, maar niet de berichtinhoud, -grootte of van wie het afkomstig is."; /* No comment provided by engineer. */ -"**Scan QR code**: to connect to your contact in person or via video call." = "**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contactpersoon."; +"**Scan QR code**: to connect to your contact in person or via video call." = "**Scan QR-code**: om persoonlijk of via een video gesprek verbinding te maken met uw contact."; /* No comment provided by engineer. */ "**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Waarschuwing**: voor directe push meldingen is een wachtwoord vereist dat is opgeslagen in de Keychain."; @@ -88,6 +91,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*vetgedrukt*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Geschiedenis"; + +/* copied message info */ +"## In reply to" = "## Als antwoord op"; + /* No comment provided by engineer. */ "#secret#" = "#geheim#"; @@ -106,6 +118,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ en %@ verbonden"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ bij %2$@:"; @@ -124,6 +139,9 @@ /* notification title */ "%@ wants to connect!" = "%@ wil verbinding maken!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ en %lld andere leden hebben verbinding gemaakt"; + /* copied message info */ "%@:" = "%@:"; @@ -166,6 +184,9 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld minuten"; +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld nieuwe interface-talen"; + /* No comment provided by engineer. */ "%lld second(s)" = "%lld seconde(n)"; @@ -194,7 +215,7 @@ "%lldw" = "%lldw"; /* No comment provided by engineer. */ -"%u messages failed to decrypt." = "%u-berichten kunnen niet worden gedecodeerd."; +"%u messages failed to decrypt." = "%u berichten kunnen niet worden ontsleuteld."; /* No comment provided by engineer. */ "%u messages skipped." = "%u berichten zijn overgeslagen."; @@ -245,10 +266,7 @@ "A new contact" = "Een nieuw contact"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Er wordt een willekeurig profiel verzonden naar het contact van wie je deze link hebt ontvangen"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Er wordt een willekeurig profiel naar uw contactpersoon verzonden"; +"A new random profile will be shared." = "Een nieuw willekeurig profiel wordt gedeeld."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Er wordt een aparte TCP-verbinding gebruikt **voor elk chat profiel dat je in de app hebt**."; @@ -285,12 +303,12 @@ "Accept" = "Accepteer"; /* No comment provided by engineer. */ -"Accept contact" = "Accepteer contactpersoon"; +"Accept connection request?" = "Accepteer contact"; /* notification body */ "Accept contact request from %@?" = "Accepteer contactverzoek van %@?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Accepteer incognito"; /* call status */ @@ -363,16 +381,16 @@ "Allow" = "Toestaan"; /* No comment provided by engineer. */ -"Allow calls only if your contact allows them." = "Sta oproepen alleen toe als uw contact persoon dit toestaat."; +"Allow calls only if your contact allows them." = "Sta oproepen alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Sta verdwijnende berichten alleen toe als uw contactpersoon dit toestaat."; +"Allow disappearing messages only if your contact allows it to you." = "Sta verdwijnende berichten alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you." = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contactpersoon dit toestaat."; +"Allow irreversible message deletion only if your contact allows it to you." = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Sta berichtreacties alleen toe als uw contactpersoon dit toestaat."; +"Allow message reactions only if your contact allows them." = "Sta berichtreacties alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ "Allow message reactions." = "Sta berichtreacties toe."; @@ -393,7 +411,7 @@ "Allow to send voice messages." = "Sta toe om spraak berichten te verzenden."; /* No comment provided by engineer. */ -"Allow voice messages only if your contact allows them." = "Sta spraak berichten alleen toe als uw contactpersoon ze toestaat."; +"Allow voice messages only if your contact allows them." = "Sta spraak berichten alleen toe als uw contact ze toestaat."; /* No comment provided by engineer. */ "Allow voice messages?" = "Spraak berichten toestaan?"; @@ -431,6 +449,9 @@ /* No comment provided by engineer. */ "App build: %@" = "App build: %@"; +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "App versleutelt nieuwe lokale bestanden (behalve video's)."; + /* No comment provided by engineer. */ "App icon" = "App icon"; @@ -498,10 +519,10 @@ "Bad message hash" = "Onjuiste bericht hash"; /* integrity error chat item */ -"bad message ID" = "Onjuiste bericht ID"; +"bad message ID" = "Onjuiste bericht-ID"; /* No comment provided by engineer. */ -"Bad message ID" = "Onjuiste bericht ID"; +"Bad message ID" = "Onjuiste bericht-ID"; /* No comment provided by engineer. */ "Better messages" = "Betere berichten"; @@ -510,19 +531,22 @@ "bold" = "vetgedrukt"; /* No comment provided by engineer. */ -"Both you and your contact can add message reactions." = "Zowel u als uw contactpersoon kunnen berichtreacties toevoegen."; +"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen berichtreacties toevoegen."; /* No comment provided by engineer. */ -"Both you and your contact can irreversibly delete sent messages." = "Zowel jij als je contactpersoon kunnen verzonden berichten onherroepelijk verwijderen."; +"Both you and your contact can irreversibly delete sent messages." = "Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen."; /* No comment provided by engineer. */ -"Both you and your contact can make calls." = "Zowel u als uw contact persoon kunnen bellen."; +"Both you and your contact can make calls." = "Zowel u als uw contact kunnen bellen."; /* No comment provided by engineer. */ -"Both you and your contact can send disappearing messages." = "Zowel jij als je contactpersoon kunnen verdwijnende berichten sturen."; +"Both you and your contact can send disappearing messages." = "Zowel jij als je contact kunnen verdwijnende berichten sturen."; /* No comment provided by engineer. */ -"Both you and your contact can send voice messages." = "Zowel jij als je contactpersoon kunnen spraak berichten verzenden."; +"Both you and your contact can send voice messages." = "Zowel jij als je contact kunnen spraak berichten verzenden."; + +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bulgaars, Fins, Thais en Oekraïens - dankzij de gebruikers en [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Via chat profiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -696,11 +720,17 @@ /* server test step */ "Connect" = "Verbind"; +/* No comment provided by engineer. */ +"Connect directly" = "Verbind direct"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Verbind incognito"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "maak verbinding met SimpleX Chat-ontwikkelaars."; /* No comment provided by engineer. */ -"Connect via contact link?" = "Verbinden via contact link?"; +"Connect via contact link" = "Verbinden via contact link?"; /* No comment provided by engineer. */ "Connect via group link?" = "Verbinden via groep link?"; @@ -712,7 +742,7 @@ "Connect via link / QR code" = "Maak verbinding via link / QR-code"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Verbinden via een eenmalige link?"; +"Connect via one-time link" = "Verbinden via een eenmalige link?"; /* No comment provided by engineer. */ "connected" = "verbonden"; @@ -756,9 +786,6 @@ /* chat list item title (it should not be shown */ "connection established" = "verbinding gemaakt"; -/* No comment provided by engineer. */ -"Connection request" = "Verbindingsverzoek"; - /* No comment provided by engineer. */ "Connection request sent!" = "Verbindingsverzoek verzonden!"; @@ -828,6 +855,9 @@ /* No comment provided by engineer. */ "Create link" = "Maak link"; +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Maak een nieuw profiel aan in [desktop-app](https://simplex.chat/downloads/). 💻"; + /* No comment provided by engineer. */ "Create one-time invitation link" = "Maak een eenmalige uitnodiging link"; @@ -883,13 +913,13 @@ "Database error" = "Database fout"; /* No comment provided by engineer. */ -"Database ID" = "Database ID"; +"Database ID" = "Database-ID"; /* copied message info */ -"Database ID: %d" = "Database ID: %d"; +"Database ID: %d" = "Database-ID: %d"; /* No comment provided by engineer. */ -"Database IDs and Transport isolation option." = "Database ID's en Transport isolatie optie."; +"Database IDs and Transport isolation option." = "Database-ID's en Transport isolatie optie."; /* No comment provided by engineer. */ "Database is encrypted using a random passphrase, you can change it." = "De database is versleuteld met een willekeurig wachtwoord, u kunt deze wijzigen."; @@ -1059,6 +1089,9 @@ /* rcv group event chat item */ "deleted group" = "verwijderde groep"; +/* No comment provided by engineer. */ +"Delivery" = "Bezorging"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "Ontvangstbewijzen zijn uitgeschakeld!"; @@ -1107,6 +1140,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "SimpleX Vergrendelen uitschakelen"; +/* No comment provided by engineer. */ +"disabled" = "uitgeschakeld"; + /* No comment provided by engineer. */ "Disappearing message" = "Verdwijnend bericht"; @@ -1120,14 +1156,17 @@ "Disappearing messages are prohibited in this group." = "Verdwijnende berichten zijn verboden in deze groep."; /* No comment provided by engineer. */ -"Disappears at" = "Verdwijnt om"; +"Disappears at" = "Verdwijnt op"; /* copied message info */ -"Disappears at: %@" = "Verdwijnt om: %@"; +"Disappears at: %@" = "Verdwijnt op: %@"; /* server test step */ "Disconnect" = "verbinding verbreken"; +/* No comment provided by engineer. */ +"Discover and join groups" = "Ontdek en sluit je aan bij groepen"; + /* No comment provided by engineer. */ "Display name" = "Weergavenaam"; @@ -1224,6 +1263,12 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Database versleutelen?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Versleutel lokale bestanden"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Versleutel opgeslagen bestanden en media"; + /* No comment provided by engineer. */ "Encrypted database" = "Versleutelde database"; @@ -1335,6 +1380,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Fout bij aanmaken van profiel!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Fout bij het ontsleutelen van bestand"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Fout bij het verwijderen van de chat database"; @@ -1360,7 +1408,7 @@ "Error deleting user profile" = "Fout bij het verwijderen van gebruikers profiel"; /* No comment provided by engineer. */ -"Error enabling delivery receipts!" = "Fout bij het inschakelen van ontvangstbevestiging!"; +"Error enabling delivery receipts!" = "Fout bij het inschakelen van ontvangst bevestiging!"; /* No comment provided by engineer. */ "Error enabling notifications" = "Fout bij inschakelen van meldingen"; @@ -1411,7 +1459,7 @@ "Error sending message" = "Fout bij verzenden van bericht"; /* No comment provided by engineer. */ -"Error setting delivery receipts!" = "Fout bij het instellen van ontvangstbevestiging!"; +"Error setting delivery receipts!" = "Fout bij het instellen van ontvangst bevestiging!"; /* No comment provided by engineer. */ "Error starting chat" = "Fout bij het starten van de chat"; @@ -1452,6 +1500,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; +/* No comment provided by engineer. */ +"event happened" = "gebeurtenis gebeurd"; + /* No comment provided by engineer. */ "Exit without saving" = "Afsluiten zonder opslaan"; @@ -1480,10 +1531,10 @@ "File will be deleted from servers." = "Het bestand wordt van de servers verwijderd."; /* No comment provided by engineer. */ -"File will be received when your contact completes uploading it." = "Het bestand wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid."; +"File will be received when your contact completes uploading it." = "Het bestand wordt gedownload wanneer uw contact het uploaden heeft voltooid."; /* No comment provided by engineer. */ -"File will be received when your contact is online, please wait or check later!" = "Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!"; +"File will be received when your contact is online, please wait or check later!" = "Het bestand wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of controleer later!"; /* No comment provided by engineer. */ "File: %@" = "Bestand: %@"; @@ -1650,7 +1701,7 @@ /* No comment provided by engineer. */ "Hide:" = "Verbergen:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Geschiedenis"; /* time unit */ @@ -1678,7 +1729,7 @@ "If you can't meet in person, show QR code in a video call, or share the link." = "Als je elkaar niet persoonlijk kunt ontmoeten, laat dan de QR-code zien in een videogesprek of deel de link."; /* No comment provided by engineer. */ -"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contactpersoon kan een uitnodiging link delen."; +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Als u elkaar niet persoonlijk kunt ontmoeten, kunt u **de QR-code scannen in het video gesprek**, of uw contact kan een uitnodiging link delen."; /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd!"; @@ -1693,7 +1744,7 @@ "Ignore" = "Negeren"; /* No comment provided by engineer. */ -"Image will be received when your contact completes uploading it." = "De afbeelding wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid."; +"Image will be received when your contact completes uploading it." = "De afbeelding wordt gedownload wanneer uw contact het uploaden heeft voltooid."; /* No comment provided by engineer. */ "Image will be received when your contact is online, please wait or check later!" = "De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!"; @@ -1719,7 +1770,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Verbeterde serverconfiguratie"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "In antwoord op"; /* No comment provided by engineer. */ @@ -1729,13 +1780,10 @@ "Incognito mode" = "Incognito modus"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "Incognito modus wordt hier niet ondersteund, uw hoofdprofiel wordt naar groepsleden verzonden"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "De incognito modus beschermt de privacy van uw hoofdprofielnaam en afbeelding, voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Incognito -modus beschermt uw privacy met behulp van een nieuw willekeurig profiel voor elk contact."; /* chat list item description */ -"incognito via contact address link" = "incognito via contact adres link"; +"incognito via contact address link" = "incognito via contactadres link"; /* chat list item description */ "incognito via group link" = "incognito via groep link"; @@ -1797,6 +1845,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Ongeldig server adres!"; +/* item status text */ +"Invalid status" = "Ongeldige status"; + /* No comment provided by engineer. */ "Invitation expired!" = "Uitnodiging verlopen!"; @@ -1843,10 +1894,10 @@ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Het maakt het mogelijk om veel anonieme verbindingen te hebben zonder enige gedeelde gegevens tussen hen in een enkel chat profiel."; /* No comment provided by engineer. */ -"It can happen when you or your connection used the old database backup." = "Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte."; +"It can happen when you or your connection used the old database backup." = "Het kan gebeuren wanneer u of de ander een oude databaseback-up gebruikt."; /* No comment provided by engineer. */ -"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Het kan gebeuren wanneer:\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.\n2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt.\n3. De verbinding is verbroken."; +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Het kan gebeuren wanneer:\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.\n2. Decodering van het bericht is mislukt, omdat u of uw contact een oude databaseback-up heeft gebruikt.\n3. De verbinding is verbroken."; /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Het lijkt erop dat u al bent verbonden via deze link. Als dit niet het geval is, is er een fout opgetreden (%@)."; @@ -1948,7 +1999,7 @@ "Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn."; /* No comment provided by engineer. */ -"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Veel mensen vroegen: *als SimpleX geen gebruikers ID's heeft, hoe kan het dan berichten bezorgen?*"; +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Veel mensen vroegen: *als SimpleX geen gebruikers-ID's heeft, hoe kan het dan berichten bezorgen?*"; /* No comment provided by engineer. */ "Mark deleted for everyone" = "Markeer verwijderd voor iedereen"; @@ -1986,11 +2037,11 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Gebruiker wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Fout bij bezorging van bericht"; /* No comment provided by engineer. */ -"Message delivery receipts!" = "Ontvangstbevestiging voor berichten!"; +"Message delivery receipts!" = "Ontvangst bevestiging voor berichten!"; /* No comment provided by engineer. */ "Message draft" = "Concept bericht"; @@ -2058,6 +2109,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Meer verbeteringen volgen snel!"; +/* item status description */ +"Most likely this connection is deleted." = "Hoogstwaarschijnlijk is deze verbinding verwijderd."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd."; @@ -2094,6 +2148,9 @@ /* No comment provided by engineer. */ "New database archive" = "Nieuw database archief"; +/* No comment provided by engineer. */ +"New desktop app!" = "Nieuwe desktop app!"; + /* No comment provided by engineer. */ "New display name" = "Nieuwe weergavenaam"; @@ -2130,6 +2187,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Geen contacten om toe te voegen"; +/* No comment provided by engineer. */ +"No delivery information" = "Geen bezorging informatie"; + /* No comment provided by engineer. */ "No device token!" = "Geen apparaattoken!"; @@ -2222,7 +2282,7 @@ "Only you can add message reactions." = "Alleen jij kunt berichtreacties toevoegen."; /* No comment provided by engineer. */ -"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering)."; +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering)."; /* No comment provided by engineer. */ "Only you can make calls." = "Alleen jij kunt bellen."; @@ -2234,19 +2294,19 @@ "Only you can send voice messages." = "Alleen jij kunt spraak berichten verzenden."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Alleen uw contactpersoon kan berichtreacties toevoegen."; +"Only your contact can add message reactions." = "Alleen uw contact kan berichtreacties toevoegen."; /* No comment provided by engineer. */ -"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering)."; +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering)."; /* No comment provided by engineer. */ "Only your contact can make calls." = "Alleen je contact kan bellen."; /* No comment provided by engineer. */ -"Only your contact can send disappearing messages." = "Alleen uw contactpersoon kan verdwijnende berichten verzenden."; +"Only your contact can send disappearing messages." = "Alleen uw contact kan verdwijnende berichten verzenden."; /* No comment provided by engineer. */ -"Only your contact can send voice messages." = "Alleen uw contactpersoon kan spraak berichten verzenden."; +"Only your contact can send voice messages." = "Alleen uw contact kan spraak berichten verzenden."; /* No comment provided by engineer. */ "Open chat" = "Gesprekken openen"; @@ -2302,8 +2362,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Plak de ontvangen link"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contact."; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; @@ -2324,10 +2384,10 @@ "PING interval" = "PING interval"; /* No comment provided by engineer. */ -"Please ask your contact to enable sending voice messages." = "Vraag uw contactpersoon om het verzenden van spraak berichten in te schakelen."; +"Please ask your contact to enable sending voice messages." = "Vraag uw contact om het verzenden van spraak berichten in te schakelen."; /* No comment provided by engineer. */ -"Please check that you used the correct link or ask your contact to send you another one." = "Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen."; +"Please check that you used the correct link or ask your contact to send you another one." = "Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen."; /* No comment provided by engineer. */ "Please check your network connection with %@ and try again." = "Controleer uw netwerkverbinding met %@ en probeer het opnieuw."; @@ -2461,6 +2521,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Lees meer in onze GitHub repository."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; + /* No comment provided by engineer. */ "received answer…" = "antwoord gekregen…"; @@ -2510,7 +2573,7 @@ "Reject" = "Afwijzen"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Contact afwijzen (afzender NIET op de hoogte)"; +"Reject (sender NOT notified)" = "Contact afwijzen (afzender NIET op de hoogte)"; /* No comment provided by engineer. */ "Reject contact request" = "Contactverzoek afwijzen"; @@ -2666,7 +2729,7 @@ "Scan QR code" = "Scan QR-code"; /* No comment provided by engineer. */ -"Scan security code from your contact's app." = "Scan de beveiligingscode van de app van uw contactpersoon."; +"Scan security code from your contact's app." = "Scan de beveiligingscode van de app van uw contact."; /* No comment provided by engineer. */ "Scan server QR code" = "Scan server QR-code"; @@ -2753,19 +2816,25 @@ "Sender may have deleted the connection request." = "De afzender heeft mogelijk het verbindingsverzoek verwijderd."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Het verzenden van ontvangstbevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen."; +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen."; /* No comment provided by engineer. */ -"Sending delivery receipts will be enabled for all contacts." = "Het verzenden van ontvangstbevestiging wordt ingeschakeld voor alle contactpersonen."; +"Sending delivery receipts will be enabled for all contacts." = "Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contactpersonen."; /* No comment provided by engineer. */ "Sending file will be stopped." = "Het verzenden van het bestand wordt gestopt."; /* No comment provided by engineer. */ -"Sending receipts is disabled for %lld contacts" = "Het verzenden van ontvangstbevestiging is uitgeschakeld voor %lld-contactpersonen"; +"Sending receipts is disabled for %lld contacts" = "Het verzenden van ontvangst bevestiging is uitgeschakeld voor %lld-contactpersonen"; /* No comment provided by engineer. */ -"Sending receipts is enabled for %lld contacts" = "Het verzenden van ontvangstbevestiging is ingeschakeld voor %lld-contactpersonen"; +"Sending receipts is disabled for %lld groups" = "Het verzenden van bevestigingen is uitgeschakeld voor %LLD -groepen"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Het verzenden van ontvangst bevestiging is ingeschakeld voor %lld-contactpersonen"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Het verzenden van bevestigingen is ingeschakeld voor %LLD -groepen"; /* No comment provided by engineer. */ "Sending via" = "Verzenden via"; @@ -2851,6 +2920,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Ontwikkelaars opties tonen"; +/* No comment provided by engineer. */ +"Show last messages" = "Laat laatste berichten zien"; + /* No comment provided by engineer. */ "Show preview" = "Toon voorbeeld"; @@ -2893,12 +2965,18 @@ /* simplex link type */ "SimpleX one-time invitation" = "Eenmalige SimpleX uitnodiging"; +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Vereenvoudigde incognitomodus"; + /* No comment provided by engineer. */ "Skip" = "Overslaan"; /* No comment provided by engineer. */ "Skipped messages" = "Overgeslagen berichten"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Kleine groepen (max 20)"; + /* No comment provided by engineer. */ "SMP servers" = "SMP servers"; @@ -3017,7 +3095,7 @@ "Thanks to the users – contribute via Weblate!" = "Dank aan de gebruikers – draag bij via Weblate!"; /* No comment provided by engineer. */ -"The 1st platform without any user identifiers – private by design." = "Het eerste platform zonder gebruikers ID's, privé door ontwerp."; +"The 1st platform without any user identifiers – private by design." = "Het eerste platform zonder gebruikers-ID's, privé door ontwerp."; /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen."; @@ -3029,7 +3107,7 @@ "The connection you accepted will be cancelled!" = "De door u geaccepteerde verbinding wordt geannuleerd!"; /* No comment provided by engineer. */ -"The contact you shared this link with will NOT be able to connect!" = "Het contact met wie je deze link hebt gedeeld, kan GEEN verbinding maken!"; +"The contact you shared this link with will NOT be able to connect!" = "Het contact met wie je deze link hebt gedeeld kan GEEN verbinding maken!"; /* No comment provided by engineer. */ "The created archive is available via app Settings / Database / Old database archive." = "Het aangemaakte archief is beschikbaar via app Instellingen / Database / Oud database archief."; @@ -3083,7 +3161,7 @@ "These settings are for your current profile **%@**." = "Deze instellingen zijn voor uw huidige profiel **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Ze kunnen worden overschreven in contactinstellingen"; +"They can be overridden in contact and group settings." = "Ze kunnen worden overschreven in contactinstellingen"; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Deze actie kan niet ongedaan worden gemaakt, alle ontvangen en verzonden bestanden en media worden verwijderd. Foto's met een lage resolutie blijven behouden."; @@ -3097,6 +3175,9 @@ /* notification title */ "this contact" = "dit contact"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Deze groep heeft meer dan %lld -leden, ontvangstbevestigingen worden niet verzonden."; + /* No comment provided by engineer. */ "This group no longer exists." = "Deze groep bestaat niet meer."; @@ -3107,16 +3188,13 @@ "To ask any questions and to receive updates:" = "Om vragen te stellen en updates te ontvangen:"; /* No comment provided by engineer. */ -"To connect, your contact can scan QR code or use the link in the app." = "Om verbinding te maken, kan uw contact persoon de QR-code scannen of de link in de app gebruiken."; - -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Om het profiel te vinden dat wordt gebruikt voor een incognito verbinding, tikt u op de naam van het contact of de groep bovenaan de chat."; +"To connect, your contact can scan QR code or use the link in the app." = "Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken."; /* No comment provided by engineer. */ "To make a new connection" = "Om een nieuwe verbinding te maken"; /* No comment provided by engineer. */ -"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, ID's voor berichten wachtrijen, afzonderlijk voor elk van uw contacten."; /* No comment provided by engineer. */ "To protect timezone, image/voice files use UTC." = "Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC."; @@ -3134,7 +3212,10 @@ "To support instant push notifications the chat database has to be migrated." = "Om directe push meldingen te ondersteunen, moet de chat database worden gemigreerd."; /* No comment provided by engineer. */ -"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contactpersoon te verifiëren."; +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren."; + +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Schakel incognito in tijdens het verbinden."; /* No comment provided by engineer. */ "Transport isolation" = "Transport isolation"; @@ -3157,7 +3238,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Kan spraakbericht niet opnemen"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Onverwachte fout: %@"; /* No comment provided by engineer. */ @@ -3194,7 +3275,7 @@ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Schakel de modus Niet storen in om onderbrekingen te voorkomen, tenzij u de iOS-oproepinterface gebruikt."; /* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.\nOm verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft."; +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft.\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft."; /* No comment provided by engineer. */ "Unlock" = "Ontgrendelen"; @@ -3244,12 +3325,18 @@ /* No comment provided by engineer. */ "Use chat" = "Gebruik chat"; +/* No comment provided by engineer. */ +"Use current profile" = "Gebruik het huidige profiel"; + /* No comment provided by engineer. */ "Use for new connections" = "Gebruik voor nieuwe verbindingen"; /* No comment provided by engineer. */ "Use iOS call interface" = "De iOS-oproepinterface gebruiken"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Gebruik een nieuw incognitoprofiel"; + /* No comment provided by engineer. */ "Use server" = "Gebruik server"; @@ -3278,7 +3365,7 @@ "Via browser" = "Via browser"; /* chat list item description */ -"via contact address link" = "via contact adres link"; +"via contact address link" = "via contactadres link"; /* chat list item description */ "via group link" = "via groep link"; @@ -3296,7 +3383,7 @@ "video call (not e2e encrypted)" = "video gesprek (niet e2e versleuteld)"; /* No comment provided by engineer. */ -"Video will be received when your contact completes uploading it." = "De video wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid."; +"Video will be received when your contact completes uploading it." = "De video wordt gedownload wanneer uw contact het uploaden heeft voltooid."; /* No comment provided by engineer. */ "Video will be received when your contact is online, please wait or check later!" = "De video wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!"; @@ -3479,7 +3566,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen."; /* No comment provided by engineer. */ -"You invited your contact" = "Je hebt je contactpersoon uitgenodigd"; +"You invited a contact" = "Je hebt je contact uitgenodigd"; /* No comment provided by engineer. */ "You joined this group" = "Je bent lid geworden van deze groep"; @@ -3539,7 +3626,7 @@ "you: " = "Jij: "; /* No comment provided by engineer. */ -"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Je probeert een contact met wie je een incognito profiel hebt gedeeld, uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt"; +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt"; /* No comment provided by engineer. */ "You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Je gebruikt een incognito profiel voor deze groep. Om te voorkomen dat je je hoofdprofiel deelt, is het niet toegestaan om contacten uit te nodigen"; @@ -3559,23 +3646,20 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Uw chat profiel wordt verzonden naar de groepsleden"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Uw chat profiel wordt naar uw contactpersoon verzonden"; - /* No comment provided by engineer. */ "Your chat profiles" = "Uw chat profielen"; /* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Uw contactpersoon moet online zijn om de verbinding te voltooien.\nU kunt deze verbinding verbreken en het contact verwijderen (en later proberen met een nieuwe link)."; +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Uw contact moet online zijn om de verbinding te voltooien.\nU kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link."; /* No comment provided by engineer. */ -"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Uw contactpersoon heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@)."; +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@)."; /* No comment provided by engineer. */ "Your contacts can allow full message deletion." = "Uw contacten kunnen volledige verwijdering van berichten toestaan."; /* No comment provided by engineer. */ -"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Uw contacten in SimpleX zullen het zien. U kunt dit wijzigen in Instellingen."; +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Uw contacten in SimpleX kunnen het zien. \nU kunt dit wijzigen in Instellingen."; /* No comment provided by engineer. */ "Your contacts will remain connected." = "Uw contacten blijven verbonden."; @@ -3596,10 +3680,10 @@ "Your privacy" = "Uw privacy"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten.\nSimpleX servers kunnen uw profiel niet zien."; +"Your profile **%@** will be shared." = "Uw profiel **%@** wordt gedeeld."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Je profiel wordt verzonden naar het contact van wie je deze link hebt ontvangen"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten.\nSimpleX servers kunnen uw profiel niet zien."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Uw profiel, contacten en afgeleverde berichten worden op uw apparaat opgeslagen."; diff --git a/apps/ios/nl.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/nl.lproj/SimpleX--iOS--InfoPlist.strings index 6d66792600..38af6191e8 100644 --- a/apps/ios/nl.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/nl.lproj/SimpleX--iOS--InfoPlist.strings @@ -5,7 +5,7 @@ "NSCameraUsageDescription" = "SimpleX heeft camera toegang nodig om QR-codes te scannen om verbinding te maken met andere gebruikers en voor video gesprekken."; /* Privacy - Face ID Usage Description */ -"NSFaceIDUsageDescription" = "SimpleX gebruikt Face ID voor lokale authenticatie"; +"NSFaceIDUsageDescription" = "SimpleX gebruikt Face-ID voor lokale authenticatie"; /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "SimpleX heeft microfoon toegang nodig voor audio en video oproepen en om spraak berichten op te nemen."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index a866c9acc4..d80ff67d1f 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_kursywa_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- połącz do [serwera katalogowego](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- potwierdzenie dostarczenia (do 20 członków).\n- szybszy i bardziej stabilny."; + /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- bardziej stabilne dostarczanie wiadomości.\n- nieco lepsze grupy.\n- i więcej!"; @@ -88,6 +91,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*pogrubiony*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Historia"; + +/* copied message info */ +"## In reply to" = "## W odpowiedzi na"; + /* No comment provided by engineer. */ "#secret#" = "#sekret#"; @@ -106,6 +118,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ i %@ połączeni"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ o %2$@:"; @@ -124,6 +139,9 @@ /* notification title */ "%@ wants to connect!" = "%@ chce się połączyć!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ i %lld innych członków połączeni"; + /* copied message info */ "%@:" = "%@:"; @@ -166,6 +184,9 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld minut"; +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld nowe języki interfejsu"; + /* No comment provided by engineer. */ "%lld second(s)" = "%lld sekund(y)"; @@ -245,10 +266,7 @@ "A new contact" = "Nowy kontakt"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Losowy profil zostanie wysłany do kontaktu, od którego otrzymałeś ten link"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Losowy profil zostanie wysłany do Twojego kontaktu"; +"A new random profile will be shared." = "Nowy losowy profil zostanie udostępniony."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Oddzielne połączenie TCP będzie używane **dla każdego profilu czatu, który masz w aplikacji**."; @@ -285,12 +303,12 @@ "Accept" = "Akceptuj"; /* No comment provided by engineer. */ -"Accept contact" = "Akceptuj kontakt"; +"Accept connection request?" = "Zaakceptować prośbę o połączenie?"; /* notification body */ "Accept contact request from %@?" = "Zaakceptuj prośbę o kontakt od %@?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Akceptuj incognito"; /* call status */ @@ -431,6 +449,9 @@ /* No comment provided by engineer. */ "App build: %@" = "Kompilacja aplikacji: %@"; +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "Aplikacja szyfruje nowe lokalne pliki (bez filmów)."; + /* No comment provided by engineer. */ "App icon" = "Ikona aplikacji"; @@ -524,6 +545,9 @@ /* 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."; +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Bułgarski, fiński, tajski i ukraiński – dzięki użytkownikom i [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Według profilu czatu (domyślnie) lub [według połączenia](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; @@ -696,11 +720,17 @@ /* server test step */ "Connect" = "Połącz"; +/* No comment provided by engineer. */ +"Connect directly" = "Połącz bezpośrednio"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Połącz incognito"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "połącz się z deweloperami SimpleX Chat."; /* No comment provided by engineer. */ -"Connect via contact link?" = "Połączyć się przez link kontaktowy?"; +"Connect via contact link" = "Połącz przez link kontaktowy"; /* No comment provided by engineer. */ "Connect via group link?" = "Połącz się przez link grupowy?"; @@ -712,7 +742,7 @@ "Connect via link / QR code" = "Połącz się przez link / kod QR"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Połączyć się przez jednorazowy link?"; +"Connect via one-time link" = "Połącz przez jednorazowy link"; /* No comment provided by engineer. */ "connected" = "połączony"; @@ -756,9 +786,6 @@ /* chat list item title (it should not be shown */ "connection established" = "połączenie ustanowione"; -/* No comment provided by engineer. */ -"Connection request" = "Prośba o połączenie"; - /* No comment provided by engineer. */ "Connection request sent!" = "Prośba o połączenie wysłana!"; @@ -828,6 +855,9 @@ /* No comment provided by engineer. */ "Create link" = "Utwórz link"; +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Utwórz nowy profil w [aplikacji desktopowej](https://simplex.chat/downloads/). 💻"; + /* No comment provided by engineer. */ "Create one-time invitation link" = "Utwórz jednorazowy link do zaproszenia"; @@ -1059,6 +1089,9 @@ /* rcv group event chat item */ "deleted group" = "usunięta grupa"; +/* No comment provided by engineer. */ +"Delivery" = "Dostarczenie"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "Potwierdzenia dostawy są wyłączone!"; @@ -1107,6 +1140,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "Wyłącz blokadę SimpleX"; +/* No comment provided by engineer. */ +"disabled" = "wyłączony"; + /* No comment provided by engineer. */ "Disappearing message" = "Znikająca wiadomość"; @@ -1128,6 +1164,9 @@ /* server test step */ "Disconnect" = "Rozłącz"; +/* No comment provided by engineer. */ +"Discover and join groups" = "Odkrywaj i dołączaj do grup"; + /* No comment provided by engineer. */ "Display name" = "Wyświetlana nazwa"; @@ -1224,6 +1263,12 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Zaszyfrować bazę danych?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Zaszyfruj lokalne pliki"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Szyfruj przechowywane pliki i media"; + /* No comment provided by engineer. */ "Encrypted database" = "Zaszyfrowana baza danych"; @@ -1335,6 +1380,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Błąd tworzenia profilu!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Błąd odszyfrowania pliku"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Błąd usuwania bazy danych czatu"; @@ -1452,6 +1500,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie."; +/* No comment provided by engineer. */ +"event happened" = "nowe wydarzenie"; + /* No comment provided by engineer. */ "Exit without saving" = "Wyjdź bez zapisywania"; @@ -1650,7 +1701,7 @@ /* No comment provided by engineer. */ "Hide:" = "Ukryj:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "Historia"; /* time unit */ @@ -1719,7 +1770,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Ulepszona konfiguracja serwera"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "W odpowiedzi na"; /* No comment provided by engineer. */ @@ -1729,10 +1780,7 @@ "Incognito mode" = "Tryb incognito"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "Tryb Incognito nie jest tutaj obsługiwany - główny profil zostanie wysłany do członków grupy"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Tryb incognito chroni prywatność nazwy i obrazu głównego profilu — dla każdego nowego kontaktu tworzony jest nowy losowy profil."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Tryb incognito chroni Twoją prywatność używając nowego losowego profilu dla każdego kontaktu."; /* chat list item description */ "incognito via contact address link" = "incognito poprzez link adresu kontaktowego"; @@ -1797,6 +1845,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Nieprawidłowy adres serwera!"; +/* item status text */ +"Invalid status" = "Nieprawidłowy status"; + /* No comment provided by engineer. */ "Invitation expired!" = "Zaproszenie wygasło!"; @@ -1986,7 +2037,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Członek zostanie usunięty z grupy - nie można tego cofnąć!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Błąd dostarczenia wiadomości"; /* No comment provided by engineer. */ @@ -2058,6 +2109,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Więcej ulepszeń już wkrótce!"; +/* item status description */ +"Most likely this connection is deleted." = "Najprawdopodobniej to połączenie jest usunięte."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Najprawdopodobniej ten kontakt usunął połączenie z Tobą."; @@ -2094,6 +2148,9 @@ /* No comment provided by engineer. */ "New database archive" = "Nowe archiwum bazy danych"; +/* No comment provided by engineer. */ +"New desktop app!" = "Nowa aplikacja desktopowa!"; + /* No comment provided by engineer. */ "New display name" = "Nowa wyświetlana nazwa"; @@ -2130,6 +2187,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Brak kontaktów do dodania"; +/* No comment provided by engineer. */ +"No delivery information" = "Brak informacji dostawy"; + /* No comment provided by engineer. */ "No device token!" = "Brak tokenu urządzenia!"; @@ -2302,8 +2362,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Wklej otrzymany link"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem."; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; @@ -2315,7 +2375,7 @@ "Periodically" = "Okresowo"; /* message decrypt error item */ -"Permanent decryption error" = "Błąd odszyfrowania"; +"Permanent decryption error" = "Stały błąd odszyfrowania"; /* No comment provided by engineer. */ "PING count" = "Liczba PINGÓW"; @@ -2461,6 +2521,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Przeczytaj więcej na naszym repozytorium GitHub."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Potwierdzenia są wyłączone"; + /* No comment provided by engineer. */ "received answer…" = "otrzymano odpowiedź…"; @@ -2510,7 +2573,7 @@ "Reject" = "Odrzuć"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Odrzuć kontakt (nadawca NIE został powiadomiony)"; +"Reject (sender NOT notified)" = "Odrzuć kontakt (nadawca NIE został powiadomiony)"; /* No comment provided by engineer. */ "Reject contact request" = "Odrzuć prośbę kontaktu"; @@ -2764,9 +2827,15 @@ /* No comment provided by engineer. */ "Sending receipts is disabled for %lld contacts" = "Wysyłanie potwierdzeń jest wyłączone dla %lld kontaktów"; +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Wysyłanie potwierdzeń jest wyłączone dla %lld grup"; + /* No comment provided by engineer. */ "Sending receipts is enabled for %lld contacts" = "Wysyłanie potwierdzeń jest włączone dla %lld kontaktów"; +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Wysyłanie potwierdzeń jest włączone dla %lld grup"; + /* No comment provided by engineer. */ "Sending via" = "Wysyłanie przez"; @@ -2851,6 +2920,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Pokaż opcje dewelopera"; +/* No comment provided by engineer. */ +"Show last messages" = "Pokaż ostatnie wiadomości"; + /* No comment provided by engineer. */ "Show preview" = "Pokaż podgląd"; @@ -2893,12 +2965,18 @@ /* simplex link type */ "SimpleX one-time invitation" = "Zaproszenie jednorazowe SimpleX"; +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Uproszczony tryb incognito"; + /* No comment provided by engineer. */ "Skip" = "Pomiń"; /* No comment provided by engineer. */ "Skipped messages" = "Pominięte wiadomości"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Małe grupy (maks. 20)"; + /* No comment provided by engineer. */ "SMP servers" = "Serwery SMP"; @@ -3083,7 +3161,7 @@ "These settings are for your current profile **%@**." = "Te ustawienia dotyczą Twojego bieżącego profilu **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Można je nadpisać w ustawieniach kontaktu"; +"They can be overridden in contact and group settings." = "Można je nadpisać w ustawieniach kontaktu."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną."; @@ -3097,6 +3175,9 @@ /* notification title */ "this contact" = "ten kontakt"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "Ta grupa ma ponad %lld członków, potwierdzenia dostawy nie są wysyłane."; + /* No comment provided by engineer. */ "This group no longer exists." = "Ta grupa już nie istnieje."; @@ -3109,9 +3190,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Aby się połączyć, Twój kontakt może zeskanować kod QR lub skorzystać z linku w aplikacji."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Aby znaleźć profil używany do połączenia incognito, dotknij nazwę kontaktu lub grupy w górnej części czatu."; - /* No comment provided by engineer. */ "To make a new connection" = "Aby nawiązać nowe połączenie"; @@ -3136,6 +3214,9 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach."; +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Przełącz incognito przy połączeniu."; + /* No comment provided by engineer. */ "Transport isolation" = "Izolacja transportu"; @@ -3157,7 +3238,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Nie można nagrać wiadomości głosowej"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Nieoczekiwany błąd: %@"; /* No comment provided by engineer. */ @@ -3244,12 +3325,18 @@ /* No comment provided by engineer. */ "Use chat" = "Użyj czatu"; +/* No comment provided by engineer. */ +"Use current profile" = "Użyj obecnego profilu"; + /* No comment provided by engineer. */ "Use for new connections" = "Użyj dla nowych połączeń"; /* No comment provided by engineer. */ "Use iOS call interface" = "Użyj interfejsu połączeń iOS"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Użyj nowego profilu incognito"; + /* No comment provided by engineer. */ "Use server" = "Użyj serwera"; @@ -3479,7 +3566,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu."; /* No comment provided by engineer. */ -"You invited your contact" = "Zaprosiłeś swój kontakt"; +"You invited a contact" = "Zaprosiłeś swój kontakt"; /* No comment provided by engineer. */ "You joined this group" = "Dołączyłeś do tej grupy"; @@ -3559,9 +3646,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Twój profil czatu zostanie wysłany do członków grupy"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Twój profil czatu zostanie wysłany do Twojego kontaktu"; - /* No comment provided by engineer. */ "Your chat profiles" = "Twoje profile czatu"; @@ -3596,10 +3680,10 @@ "Your privacy" = "Twoja prywatność"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom.\nSerwery SimpleX nie mogą zobaczyć Twojego profilu."; +"Your profile **%@** will be shared." = "Twój profil **%@** zostanie udostępniony."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Twój profil zostanie wysłany do kontaktu, od którego otrzymałeś ten link"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Twój profil jest przechowywany na urządzeniu i udostępniany tylko Twoim kontaktom.\nSerwery SimpleX nie mogą zobaczyć Twojego profilu."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Twój profil, kontakty i dostarczone wiadomości są przechowywane na Twoim urządzeniu."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 9a75d6c44c..7857870472 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_курсив_"; +/* No comment provided by engineer. */ +"- connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable." = "- соединиться с [каталогом групп](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)!\n- отчеты о доставке (до 20 членов).\n- быстрее и стабильнее."; + /* No comment provided by engineer. */ "- more stable message delivery.\n- a bit better groups.\n- and more!" = "- более стабильная доставка сообщений.\n- немного улучшенные группы.\n- и прочее!"; @@ -88,6 +91,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*жирный*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## История"; + +/* copied message info */ +"## In reply to" = "## В ответ на"; + /* No comment provided by engineer. */ "#secret#" = "#секрет#"; @@ -106,6 +118,9 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ и %@ соединены"; + /* copied message info, <sender> at <time> */ "%@ at %@:" = "%1$@ в %2$@:"; @@ -124,6 +139,9 @@ /* notification title */ "%@ wants to connect!" = "%@ хочет соединиться!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ и %lld других членов соединены"; + /* copied message info */ "%@:" = "%@:"; @@ -166,6 +184,9 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld минуты"; +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld новых языков интерфейса"; + /* No comment provided by engineer. */ "%lld second(s)" = "%lld секунд"; @@ -245,10 +266,7 @@ "A new contact" = "Новый контакт"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "Контакту, от которого Вы получили эту ссылку, будет отправлен случайный профиль"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "Вашему контакту будет отправлен случайный профиль"; +"A new random profile will be shared." = "Будет отправлен новый случайный профиль."; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "Отдельное TCP-соединение будет использоваться **для каждого профиля чата, который Вы имеете в приложении**."; @@ -285,12 +303,12 @@ "Accept" = "Принять"; /* No comment provided by engineer. */ -"Accept contact" = "Принять запрос"; +"Accept connection request?" = "Принять запрос?"; /* notification body */ "Accept contact request from %@?" = "Принять запрос на соединение от %@?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "Принять инкогнито"; /* call status */ @@ -431,6 +449,9 @@ /* No comment provided by engineer. */ "App build: %@" = "Сборка приложения: %@"; +/* No comment provided by engineer. */ +"App encrypts new local files (except videos)." = "Приложение шифрует новые локальные файлы (кроме видео)."; + /* No comment provided by engineer. */ "App icon" = "Иконка"; @@ -524,6 +545,9 @@ /* No comment provided by engineer. */ "Both you and your contact can send voice messages." = "Вы и Ваш контакт можете отправлять голосовые сообщения."; +/* No comment provided by engineer. */ +"Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Болгарский, финский, тайский и украинский - благодаря пользователям и [Weblate] (https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА)."; @@ -696,11 +720,17 @@ /* server test step */ "Connect" = "Соединиться"; +/* No comment provided by engineer. */ +"Connect directly" = "Соединиться напрямую"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Соединиться Инкогнито"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "соединитесь с разработчиками."; /* No comment provided by engineer. */ -"Connect via contact link?" = "Соединиться через ссылку-контакт?"; +"Connect via contact link" = "Соединиться через ссылку-контакт"; /* No comment provided by engineer. */ "Connect via group link?" = "Соединиться через ссылку группы?"; @@ -712,7 +742,7 @@ "Connect via link / QR code" = "Соединиться через ссылку / QR код"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "Соединиться через одноразовую ссылку?"; +"Connect via one-time link" = "Соединиться через одноразовую ссылку"; /* No comment provided by engineer. */ "connected" = "соединение установлено"; @@ -756,9 +786,6 @@ /* chat list item title (it should not be shown */ "connection established" = "соединение установлено"; -/* No comment provided by engineer. */ -"Connection request" = "Запрос на соединение"; - /* No comment provided by engineer. */ "Connection request sent!" = "Запрос на соединение отправлен!"; @@ -828,6 +855,9 @@ /* No comment provided by engineer. */ "Create link" = "Создать ссылку"; +/* No comment provided by engineer. */ +"Create new profile in [desktop app](https://simplex.chat/downloads/). 💻" = "Создайте новый профиль в [приложении для компьютера](https://simplex.chat/downloads/). 💻"; + /* No comment provided by engineer. */ "Create one-time invitation link" = "Создать ссылку-приглашение"; @@ -1059,6 +1089,9 @@ /* rcv group event chat item */ "deleted group" = "удалил(а) группу"; +/* No comment provided by engineer. */ +"Delivery" = "Доставка"; + /* No comment provided by engineer. */ "Delivery receipts are disabled!" = "Отчёты о доставке выключены!"; @@ -1107,6 +1140,9 @@ /* authentication reason */ "Disable SimpleX Lock" = "Отключить блокировку SimpleX"; +/* No comment provided by engineer. */ +"disabled" = "выключено"; + /* No comment provided by engineer. */ "Disappearing message" = "Исчезающее сообщение"; @@ -1128,6 +1164,9 @@ /* server test step */ "Disconnect" = "Разрыв соединения"; +/* No comment provided by engineer. */ +"Discover and join groups" = "Найдите и вступите в группы"; + /* No comment provided by engineer. */ "Display name" = "Имя профиля"; @@ -1224,6 +1263,12 @@ /* No comment provided by engineer. */ "Encrypt database?" = "Зашифровать базу данных?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "Шифровать локальные файлы"; + +/* No comment provided by engineer. */ +"Encrypt stored files & media" = "Шифруйте сохраненные файлы и медиа"; + /* No comment provided by engineer. */ "Encrypted database" = "База данных зашифрована"; @@ -1335,6 +1380,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "Ошибка создания профиля!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "Ошибка расшифровки файла"; + /* No comment provided by engineer. */ "Error deleting chat database" = "Ошибка при удалении данных чата"; @@ -1452,6 +1500,9 @@ /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Даже когда они выключены в разговоре."; +/* No comment provided by engineer. */ +"event happened" = "событие произошло"; + /* No comment provided by engineer. */ "Exit without saving" = "Выйти без сохранения"; @@ -1650,7 +1701,7 @@ /* No comment provided by engineer. */ "Hide:" = "Скрыть:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "История"; /* time unit */ @@ -1719,7 +1770,7 @@ /* No comment provided by engineer. */ "Improved server configuration" = "Улучшенная конфигурация серверов"; -/* copied message info */ +/* No comment provided by engineer. */ "In reply to" = "В ответ на"; /* No comment provided by engineer. */ @@ -1729,10 +1780,7 @@ "Incognito mode" = "Режим Инкогнито"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "Режим Инкогнито здесь не поддерживается - Ваш основной профиль будет отправлен членам группы"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "Режим Инкогнито защищает конфиденциальность имени и изображения Вашего основного профиля — для каждого нового контакта создается новый случайный профиль."; +"Incognito mode protects your privacy by using a new random profile for each contact." = "Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создается новый случайный профиль."; /* chat list item description */ "incognito via contact address link" = "инкогнито через ссылку-контакт"; @@ -1797,6 +1845,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "Ошибка в адресе сервера!"; +/* item status text */ +"Invalid status" = "Неверный статус"; + /* No comment provided by engineer. */ "Invitation expired!" = "Приглашение истекло!"; @@ -1986,7 +2037,7 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "Ошибка доставки сообщения"; /* No comment provided by engineer. */ @@ -2041,7 +2092,7 @@ "Moderate" = "Модерировать"; /* moderated chat item */ -"moderated" = "удалено"; +"moderated" = "модерировано"; /* No comment provided by engineer. */ "Moderated at" = "Модерировано"; @@ -2058,6 +2109,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Дополнительные улучшения скоро!"; +/* item status description */ +"Most likely this connection is deleted." = "Скорее всего, соединение удалено."; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "Скорее всего, этот контакт удалил соединение с Вами."; @@ -2094,6 +2148,9 @@ /* No comment provided by engineer. */ "New database archive" = "Новый архив чата"; +/* No comment provided by engineer. */ +"New desktop app!" = "Приложение для компьютера!"; + /* No comment provided by engineer. */ "New display name" = "Новое имя"; @@ -2130,6 +2187,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "Нет контактов для добавления"; +/* No comment provided by engineer. */ +"No delivery information" = "Нет информации от доставке"; + /* No comment provided by engineer. */ "No device token!" = "Отсутствует токен устройства!"; @@ -2302,8 +2362,8 @@ /* No comment provided by engineer. */ "Paste received link" = "Вставить полученную ссылку"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "Чтобы соединиться, вставьте ссылку, полученную от Вашего контакта."; +/* placeholder */ +"Paste the link you received to connect with your contact." = "Чтобы соединиться, вставьте ссылку, полученную от Вашего контакта."; /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; @@ -2461,6 +2521,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "Узнайте больше из нашего GitHub репозитория."; +/* No comment provided by engineer. */ +"Receipts are disabled" = "Отчёты о доставке выключены"; + /* No comment provided by engineer. */ "received answer…" = "получен ответ…"; @@ -2510,7 +2573,7 @@ "Reject" = "Отклонить"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "Отклонить (не уведомляя отправителя)"; +"Reject (sender NOT notified)" = "Отклонить (не уведомляя отправителя)"; /* No comment provided by engineer. */ "Reject contact request" = "Отклонить запрос"; @@ -2764,9 +2827,15 @@ /* No comment provided by engineer. */ "Sending receipts is disabled for %lld contacts" = "Отправка отчётов о доставке выключена для %lld контактов"; +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Отчёты о доставке выключены для %lld групп"; + /* No comment provided by engineer. */ "Sending receipts is enabled for %lld contacts" = "Отправка отчётов о доставке включена для %lld контактов"; +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Отчёты о доставке включены для %lld групп"; + /* No comment provided by engineer. */ "Sending via" = "Отправка через"; @@ -2851,6 +2920,9 @@ /* No comment provided by engineer. */ "Show developer options" = "Показать опции для девелоперов"; +/* No comment provided by engineer. */ +"Show last messages" = "Показывать последние сообщения"; + /* No comment provided by engineer. */ "Show preview" = "Показывать уведомления"; @@ -2893,12 +2965,18 @@ /* simplex link type */ "SimpleX one-time invitation" = "SimpleX одноразовая ссылка"; +/* No comment provided by engineer. */ +"Simplified incognito mode" = "Упрощенный режим Инкогнито"; + /* No comment provided by engineer. */ "Skip" = "Пропустить"; /* No comment provided by engineer. */ "Skipped messages" = "Пропущенные сообщения"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Маленькие группы (до 20)"; + /* No comment provided by engineer. */ "SMP servers" = "SMP серверы"; @@ -3083,7 +3161,7 @@ "These settings are for your current profile **%@**." = "Установки для Вашего активного профиля **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact settings" = "Они могут быть переопределены в настройках контактов"; +"They can be overridden in contact and group settings." = "Они могут быть переопределены в настройках контактов и групп."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Это действие нельзя отменить — все полученные и отправленные файлы будут удалены. Изображения останутся в низком разрешении."; @@ -3097,6 +3175,9 @@ /* notification title */ "this contact" = "этот контакт"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "В группе более %lld членов, отчёты о доставке выключены."; + /* No comment provided by engineer. */ "This group no longer exists." = "Эта группа больше не существует."; @@ -3109,9 +3190,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "Чтобы соединиться с Вами, Ваш контакт может отсканировать QR-код или использовать ссылку в приложении."; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата."; - /* No comment provided by engineer. */ "To make a new connection" = "Чтобы соединиться"; @@ -3136,6 +3214,9 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах."; +/* No comment provided by engineer. */ +"Toggle incognito when connecting." = "Установите режим Инкогнито при соединении."; + /* No comment provided by engineer. */ "Transport isolation" = "Отдельные сессии для"; @@ -3157,7 +3238,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Невозможно записать голосовое сообщение"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "Неожиданная ошибка: %@"; /* No comment provided by engineer. */ @@ -3244,12 +3325,18 @@ /* No comment provided by engineer. */ "Use chat" = "Использовать чат"; +/* No comment provided by engineer. */ +"Use current profile" = "Использовать активный профиль"; + /* No comment provided by engineer. */ "Use for new connections" = "Использовать для новых соединений"; /* No comment provided by engineer. */ "Use iOS call interface" = "Использовать интерфейс iOS для звонков"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "Использовать новый Инкогнито профиль"; + /* No comment provided by engineer. */ "Use server" = "Использовать сервер"; @@ -3479,7 +3566,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата."; /* No comment provided by engineer. */ -"You invited your contact" = "Вы пригласили Ваш контакт"; +"You invited a contact" = "Вы пригласили контакт"; /* No comment provided by engineer. */ "You joined this group" = "Вы вступили в эту группу"; @@ -3559,9 +3646,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "Ваш профиль чата будет отправлен членам группы"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "Ваш профиль будет отправлен Вашему контакту"; - /* No comment provided by engineer. */ "Your chat profiles" = "Ваши профили чата"; @@ -3596,10 +3680,10 @@ "Your privacy" = "Конфиденциальность"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам.\nSimpleX серверы не могут получить доступ к Вашему профилю."; +"Your profile **%@** will be shared." = "Будет отправлен Ваш профиль **%@**."; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "Ваш профиль будет отправлен Вашему контакту."; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профиль хранится на Вашем устройстве и отправляется только Вашим контактам.\nSimpleX серверы не могут получить доступ к Вашему профилю."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Ваш профиль, контакты и доставленные сообщения хранятся на Вашем устройстве."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings new file mode 100644 index 0000000000..d886ece0d6 --- /dev/null +++ b/apps/ios/th.lproj/Localizable.strings @@ -0,0 +1,3582 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" (" = " ("; + +/* No comment provided by engineer. */ +" (can be copied)" = " (สามารถคัดลอกได้)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_ตัวเอียง_"; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- การส่งข้อความมีเสถียรภาพมากขึ้น\n- กลุ่มที่ดีขึ้นเล็กน้อย\n- และอื่น ๆ!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- ข้อความเสียงนานสุด 5 นาที\n- เวลาที่กำหนดเองที่จะหายไป\n- ประวัติการแก้ไข"; + +/* No comment provided by engineer. */ +", " = ", "; + +/* No comment provided by engineer. */ +": " = ": "; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 มีสี!"; + +/* No comment provided by engineer. */ +"." = "."; + +/* No comment provided by engineer. */ +"(" = "("; + +/* No comment provided by engineer. */ +")" = ")"; + +/* No comment provided by engineer. */ +"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[มีส่วนร่วม](https://github.com/simplex-chat/simplex-chat#contribute)"; + +/* No comment provided by engineer. */ +"[Send us email](mailto:chat@simplex.chat)" = "[ส่งอีเมลถึงเรา](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[ติดดาวบน GitHub](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**เพิ่มผู้ติดต่อใหม่**: เพื่อสร้างคิวอาร์โค้ดแบบใช้ครั้งเดียวหรือลิงก์สำหรับผู้ติดต่อของคุณ"; + +/* No comment provided by engineer. */ +"**Create link / QR code** for your contact to use." = "**สร้างลิงค์ / คิวอาร์โค้ด** เพื่อให้ผู้ติดต่อของคุณใช้"; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "การโทรเสียงแบบ **encrypted จากต้นจนจบ**"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**encrypted จากต้นจนจบ** การสนทนาทางวิดีโอ"; + +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**เป็นส่วนตัวมากขึ้น**: ตรวจสอบข้อความใหม่ทุกๆ 20 นาที โทเค็นอุปกรณ์แชร์กับเซิร์ฟเวอร์ SimpleX Chat แต่ไม่ระบุจำนวนผู้ติดต่อหรือข้อความที่คุณมี"; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**ส่วนตัวที่สุด**: ไม่ใช้เซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat ตรวจสอบข้อความเป็นระยะในพื้นหลัง (ขึ้นอยู่กับความถี่ที่คุณใช้แอป)"; + +/* No comment provided by engineer. */ +"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**แปะลิงก์ที่ได้รับ** หรือเปิดในเบราว์เซอร์แล้วแตะ **เปิดในแอปมือถือ**"; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**โปรดทราบ**: คุณจะไม่สามารถกู้คืนหรือเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย"; + +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**แนะนำ**: โทเค็นอุปกรณ์และการแจ้งเตือนจะถูกส่งไปยังเซิร์ฟเวอร์การแจ้งเตือนของ SimpleX Chat แต่ไม่ใช่เนื้อหาข้อความ ขนาด หรือผู้ที่ส่ง"; + +/* No comment provided by engineer. */ +"**Scan QR code**: to connect to your contact in person or via video call." = "**สแกนคิวอาร์โค้ด**: เพื่อเชื่อมต่อกับผู้ติดต่อของคุณด้วยตนเองหรือผ่านการสนทนาทางวิดีโอ"; + +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**คำเตือน**: การแจ้งเตือนแบบพุชทันทีจำเป็นต้องบันทึกรหัสผ่านไว้ใน Keychain"; + +/* No comment provided by engineer. */ +"*bold*" = "\\*ตัวหนา*"; + +/* No comment provided by engineer. */ +"#secret#" = "#ความลับ#"; + +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (ปัจจุบัน)"; + +/* copied message info */ +"%@ (current):" = "%@ (ปัจจุบัน):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / %@"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ %@"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ ที่ %2$@:"; + +/* notification title */ +"%@ is connected!" = "%@ เชื่อมต่อสำเร็จ!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ ไม่ได้รับการยืนยัน"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ ได้รับการตรวจสอบแล้ว"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ เซิร์ฟเวอร์"; + +/* notification title */ +"%@ wants to connect!" = "%@ อยากเชื่อมต่อ!"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d วัน"; + +/* time interval */ +"%d hours" = "%d ชั่วโมง"; + +/* time interval */ +"%d min" = "%d นาที"; + +/* time interval */ +"%d months" = "%d เดือน"; + +/* time interval */ +"%d sec" = "%d วินาที"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d ข้อความที่ถูกข้าม"; + +/* time interval */ +"%d weeks" = "%d สัปดาห์"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "% ผู้ติดต่อ LLD ที่เลือกไว้"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld ไฟล์ที่มีขนาดรวม %@"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld สมาชิก"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld นาที"; + +/* No comment provided by engineer. */ +"%lld second(s)" = "%lld วินาที"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld วินาที"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldd"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldh"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldm"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldmth"; + +/* No comment provided by engineer. */ +"%llds" = "%llds"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldw"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "การ decrypt %u ข้อความล้มเหลว"; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u ข้อความที่ถูกข้าม"; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>สวัสดี!</p>\n<p><a href=\"%@\">เชื่อมต่อกับฉันผ่าน SimpleX Chat</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~ตี~"; + +/* No comment provided by engineer. */ +"0s" = "0s"; + +/* time interval */ +"1 day" = "1 วัน"; + +/* time interval */ +"1 hour" = "1 ชั่วโมง"; + +/* No comment provided by engineer. */ +"1 minute" = "1 นาที"; + +/* time interval */ +"1 month" = "1 เดือน"; + +/* time interval */ +"1 week" = "1 สัปดาห์"; + +/* No comment provided by engineer. */ +"1-time link" = "ลิงก์สำหรับใช้ 1 ครั้ง"; + +/* No comment provided by engineer. */ +"5 minutes" = "5 นาที"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 วินาที"; + +/* No comment provided by engineer. */ +"A few more things" = "อีกสองสามอย่าง"; + +/* notification title */ +"A new contact" = "ผู้ติดต่อใหม่"; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "การเชื่อมต่อ TCP แบบแยกต่างหากจะถูกใช้ **สําหรับแต่ละโปรไฟล์การแชทที่คุณมีในแอป**."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "การเชื่อมต่อ TCP แบบแยกต่างหากจะถูกใช้ **สำหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่ม**.\n**โปรดทราบ**: หากคุณมีการเชื่อมต่อจำนวนมาก แบตเตอรี่และปริมาณการใช้ข้อมูลของคุณอาจสูงขึ้นอย่างมาก และการเชื่อมต่อบางอย่างอาจล้มเหลว"; + +/* No comment provided by engineer. */ +"Abort" = "ยกเลิก"; + +/* No comment provided by engineer. */ +"Abort changing address" = "ยกเลิกการเปลี่ยนที่อยู่"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "ยกเลิกการเปลี่ยนที่อยู่?"; + +/* No comment provided by engineer. */ +"About SimpleX" = "เกี่ยวกับ SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX address" = "เกี่ยวกับที่อยู่ SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "เกี่ยวกับ SimpleX Chat"; + +/* No comment provided by engineer. */ +"above, then choose:" = "ด้านบน จากนั้นเลือก:"; + +/* No comment provided by engineer. */ +"Accent color" = "สีเน้น"; + +/* accept contact request via notification + accept incoming call via notification */ +"Accept" = "รับ"; + +/* notification body */ +"Accept contact request from %@?" = "รับการขอติดต่อจาก %@?"; + +/* accept contact request via notification */ +"Accept incognito" = "ยอมรับโหมดไม่ระบุตัวตน"; + +/* call status */ +"accepted call" = "รับสายแล้ว"; + +/* 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." = "เพิ่มที่อยู่ลงในโปรไฟล์ของคุณ เพื่อให้ผู้ติดต่อของคุณสามารถแชร์กับผู้อื่นได้ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; + +/* No comment provided by engineer. */ +"Add preset servers" = "เพิ่มเซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; + +/* No comment provided by engineer. */ +"Add profile" = "เพิ่มโปรไฟล์"; + +/* No comment provided by engineer. */ +"Add server…" = "เพิ่มเซิร์ฟเวอร์…"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด"; + +/* No comment provided by engineer. */ +"Add to another device" = "เพิ่มเข้าไปในอุปกรณ์อื่น"; + +/* No comment provided by engineer. */ +"Add welcome message" = "เพิ่มข้อความต้อนรับ"; + +/* No comment provided by engineer. */ +"Address" = "ที่อยู่"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ"; + +/* member role */ +"admin" = "ผู้ดูแลระบบ"; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "ผู้ดูแลระบบสามารถสร้างลิงก์เพื่อเข้าร่วมกลุ่มต่างๆได้"; + +/* No comment provided by engineer. */ +"Advanced network settings" = "การตั้งค่าระบบเครือข่ายขั้นสูง"; + +/* chat item text */ +"agreeing encryption for %@…" = "ยอมรับ encryption สำหรับ %@…"; + +/* chat item text */ +"agreeing encryption…" = "เห็นด้วยกับการ encryption…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "ข้อมูลแอปทั้งหมดถูกลบแล้ว."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "แชทและข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน"; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "ข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่ การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ."; + +/* No comment provided by engineer. */ +"Allow" = "อนุญาต"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น."; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you." = "อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น"; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "อนุญาตการแสดงปฏิกิริยาต่อข้อความเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น"; + +/* No comment provided by engineer. */ +"Allow message reactions." = "อนุญาตการแสดงปฏิกิริยาต่อข้อความ"; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "อนุญาตการส่งข้อความโดยตรงไปยังสมาชิก"; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message)"; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages." = "อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร"; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "อนุญาตให้ส่งไฟล์และสื่อ"; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "อนุญาตให้ส่งข้อความเสียง"; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "อนุญาตข้อความเสียงเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น"; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "อนุญาตข้อความเสียงหรือไม่?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "อนุญาตให้ผู้ติดต่อของคุณเพิ่มการแสดงปฏิกิริยาต่อข้อความ"; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "อนุญาตให้ผู้ติดต่อของคุณโทรหาคุณ"; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages." = "อนุญาตให้ผู้ติดต่อของคุณลบข้อความที่ส่งแล้วอย่างถาวร"; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "อนุญาตให้ผู้ติดต่อของคุณส่งข้อความที่จะหายไปหลังปิดแชท (disappearing messages)"; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "อนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียง"; + +/* No comment provided by engineer. */ +"Already connected?" = "เชื่อมต่อสำเร็จแล้ว?"; + +/* pref value */ +"always" = "เสมอ"; + +/* No comment provided by engineer. */ +"Always use relay" = "ใช้รีเลย์เสมอ"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "โปรไฟล์แชทที่ว่างเปล่าพร้อมชื่อที่ให้ไว้ได้ถูกสร้างขึ้นและแอปจะเปิดตามปกติ"; + +/* No comment provided by engineer. */ +"Answer call" = "รับสาย"; + +/* No comment provided by engineer. */ +"App build: %@" = "รุ่นแอป: %@"; + +/* No comment provided by engineer. */ +"App icon" = "ไอคอนแอป"; + +/* No comment provided by engineer. */ +"App passcode" = "รหัสผ่านแอป"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "รหัสผ่านแอปจะถูกแทนที่ด้วยรหัสผ่านที่ทำลายตัวเอง"; + +/* No comment provided by engineer. */ +"App version" = "เวอร์ชันแอป"; + +/* No comment provided by engineer. */ +"App version: v%@" = "เวอร์ชันแอป: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "รูปร่างลักษณะ"; + +/* No comment provided by engineer. */ +"Attach" = "แนบ"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "การโทรด้วยเสียงและวิดีโอ"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "การโทรด้วยเสียงและวิดีโอ"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)"; + +/* chat feature */ +"Audio/video calls" = "การโทรด้วยเสียง/วิดีโอ"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "การโทรด้วยเสียง/วิดีโอถูกห้าม"; + +/* PIN entry */ +"Authentication cancelled" = "การยืนยันถูกยกเลิกแล้ว"; + +/* No comment provided by engineer. */ +"Authentication failed" = "การยืนยันล้มเหลว"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "การยืนยันตัวตนเป็นที่จำเป็นก่อนเชื่อมต่อสาย แต่คุณอาจไม่ได้รับสาย"; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "การยืนยันไม่พร้อมใช้งาน"; + +/* No comment provided by engineer. */ +"Auto-accept" = "ยอมรับอัตโนมัติ"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "ตอบรับคำขอเป็นเพื่อนโดยอัตโนมัติ"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "ยอมรับภาพอัตโนมัติ"; + +/* No comment provided by engineer. */ +"Back" = "กลับ"; + +/* integrity error chat item */ +"bad message hash" = "แฮชข้อความไม่ดี"; + +/* No comment provided by engineer. */ +"Bad message hash" = "แฮชข้อความไม่ดี"; + +/* integrity error chat item */ +"bad message ID" = "ID ข้อความที่ไม่ดี"; + +/* No comment provided by engineer. */ +"Bad message ID" = "ID ข้อความที่ไม่ดี"; + +/* No comment provided by engineer. */ +"Better messages" = "ข้อความที่ดีขึ้น"; + +/* No comment provided by engineer. */ +"bold" = "ตัวหนา"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้"; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages." = "ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้"; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "ทั้งคุณและผู้ติดต่อของคุณสามารถโทรออกได้"; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความที่หายไปได้"; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "ทั้งคุณและผู้ติดต่อของคุณสามารถส่งข้อความเสียงได้"; + +/* No comment provided by engineer. */ +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "ตามโปรไฟล์แชท (ค่าเริ่มต้น) หรือ [โดยการเชื่อมต่อ](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (เบต้า)"; + +/* No comment provided by engineer. */ +"Call already ended!" = "สิ้นสุดการโทรแล้ว!"; + +/* call status */ +"call error" = "การโทรผิดพลาด"; + +/* call status */ +"call in progress" = "กําลังโทร"; + +/* call status */ +"calling…" = "กำลังโทร…"; + +/* No comment provided by engineer. */ +"Calls" = "โทร"; + +/* No comment provided by engineer. */ +"Can't delete user profile!" = "ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "ไม่สามารถเชิญผู้ติดต่อได้!"; + +/* No comment provided by engineer. */ +"Cancel" = "ยกเลิก"; + +/* feature offered item */ +"cancelled %@" = "ยกเลิก %@"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Cannot receive file" = "ไม่สามารถรับไฟล์ได้"; + +/* No comment provided by engineer. */ +"Change" = "เปลี่ยน"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "เปลี่ยนรหัสผ่านฐานข้อมูล?"; + +/* authentication reason */ +"Change lock mode" = "เปลี่ยนโหมดล็อค"; + +/* No comment provided by engineer. */ +"Change member role?" = "เปลี่ยนบทบาทของสมาชิก?"; + +/* authentication reason */ +"Change passcode" = "เปลี่ยนรหัสผ่าน"; + +/* No comment provided by engineer. */ +"Change receiving address" = "เปลี่ยนที่อยู่ผู้รับ"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "เปลี่ยนที่อยู่ผู้รับ?"; + +/* No comment provided by engineer. */ +"Change role" = "เปลี่ยนบทบาท"; + +/* authentication reason */ +"Change self-destruct mode" = "เปลี่ยนโหมดทําลายตัวเอง"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "เปลี่ยนรหัสผ่านแบบทำลายตัวเอง"; + +/* chat item text */ +"changed address for you" = "เปลี่ยนที่อยู่สําหรับคุณแล้ว"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "เปลี่ยนบทบาทของ %1$@ เป็น %2$@ แล้ว"; + +/* rcv group event chat item */ +"changed your role to %@" = "เปลี่ยนบทบาทของคุณเป็น %@"; + +/* chat item text */ +"changing address for %@…" = "เปลี่ยนที่อยู่สำหรับ %@…"; + +/* chat item text */ +"changing address…" = "กำลังเปลี่ยนที่อยู่…"; + +/* No comment provided by engineer. */ +"Chat archive" = "ที่เก็บแชทถาวร"; + +/* No comment provided by engineer. */ +"Chat console" = "คอนโซลแชท"; + +/* No comment provided by engineer. */ +"Chat database" = "ฐานข้อมูลแชท"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "ลบฐานข้อมูลแชทแล้ว"; + +/* No comment provided by engineer. */ +"Chat database imported" = "นำฐานข้อมูลแชทเข้าแล้ว"; + +/* No comment provided by engineer. */ +"Chat is running" = "แชทกำลังทำงานอยู่"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "การแชทหยุดทํางานแล้ว"; + +/* No comment provided by engineer. */ +"Chat preferences" = "ค่ากําหนดในการแชท"; + +/* No comment provided by engineer. */ +"Chats" = "แชท"; + +/* No comment provided by engineer. */ +"Check server address and try again." = "ตรวจสอบที่อยู่เซิร์ฟเวอร์แล้วลองอีกครั้ง"; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "อินเทอร์เฟซภาษาจีนและสเปน"; + +/* No comment provided by engineer. */ +"Choose file" = "เลือกไฟล์"; + +/* No comment provided by engineer. */ +"Choose from library" = "เลือกจากอัลบั้ม"; + +/* No comment provided by engineer. */ +"Clear" = "ลบ"; + +/* No comment provided by engineer. */ +"Clear conversation" = "ลบการสนทนา"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "ลบการสนทนา?"; + +/* No comment provided by engineer. */ +"Clear verification" = "ล้างการยืนยัน"; + +/* No comment provided by engineer. */ +"colored" = "มีสี"; + +/* No comment provided by engineer. */ +"Colors" = "สี"; + +/* server test step */ +"Compare file" = "เปรียบเทียบไฟล์"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ"; + +/* No comment provided by engineer. */ +"complete" = "สมบูรณ์"; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "กำหนดค่าเซิร์ฟเวอร์ ICE"; + +/* No comment provided by engineer. */ +"Confirm" = "ยืนยัน"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "ยืนยันการอัพเกรดฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "ยืนยันรหัสผ่านใหม่…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "ยืนยันรหัสผ่าน"; + +/* No comment provided by engineer. */ +"Confirm password" = "ยืนยันรหัสผ่าน"; + +/* server test step */ +"Connect" = "เชื่อมต่อ"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "เชื่อมต่อกับนักพัฒนา SimpleX Chat"; + +/* No comment provided by engineer. */ +"Connect via group link?" = "เชื่อมต่อผ่านลิงค์กลุ่ม?"; + +/* No comment provided by engineer. */ +"Connect via link" = "เชื่อมต่อผ่านลิงก์"; + +/* No comment provided by engineer. */ +"Connect via link / QR code" = "เชื่อมต่อผ่านลิงค์ / คิวอาร์โค้ด"; + +/* No comment provided by engineer. */ +"connected" = "เชื่อมต่อสำเร็จ"; + +/* No comment provided by engineer. */ +"connecting" = "กำลังเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "กำลังเชื่อมต่อ (ยอมรับแล้ว)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "กำลังเชื่อมต่อ (ประกาศแล้ว)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "กำลังเชื่อมต่อ (แนะนํา)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "กำลังเชื่อมต่อ (คําเชิญแนะนํา)"; + +/* call status */ +"connecting call" = "กําลังเชื่อมต่อสาย…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "กำลังเชื่อมต่อกับเซิร์ฟเวอร์…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@)"; + +/* chat list item title */ +"connecting…" = "กำลังเชื่อมต่อ…"; + +/* No comment provided by engineer. */ +"Connection" = "การเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Connection error" = "การเชื่อมต่อผิดพลาด"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "การเชื่อมต่อผิดพลาด (AUTH)"; + +/* chat list item title (it should not be shown */ +"connection established" = "สร้างการเชื่อมต่อแล้ว"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "ส่งคําขอเชื่อมต่อแล้ว!"; + +/* No comment provided by engineer. */ +"Connection timeout" = "หมดเวลาการเชื่อมต่อ"; + +/* connection information */ +"connection:%@" = "การเชื่อมต่อ:%@"; + +/* No comment provided by engineer. */ +"Contact allows" = "ผู้ติดต่ออนุญาต"; + +/* No comment provided by engineer. */ +"Contact already exists" = "ผู้ติดต่อรายนี้มีอยู่แล้ว"; + +/* No comment provided by engineer. */ +"Contact and all messages will be deleted - this cannot be undone!" = "ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "ผู้ติดต่อมีการ encrypt จากต้นจนจบ"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "ผู้ติดต่อไม่มีการ encrypt จากต้นจนจบ"; + +/* notification */ +"Contact hidden:" = "ผู้ติดต่อถูกซ่อน:"; + +/* notification */ +"Contact is connected" = "เชื่อมต่อกับผู้ติดต่อแล้ว"; + +/* No comment provided by engineer. */ +"Contact is not connected yet!" = "ผู้ติดต่อยังไม่ได้เชื่อมต่อ!"; + +/* No comment provided by engineer. */ +"Contact name" = "ชื่อผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Contact preferences" = "การกําหนดลักษณะการติดต่อ"; + +/* No comment provided by engineer. */ +"Contacts" = "ติดต่อ"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้"; + +/* No comment provided by engineer. */ +"Continue" = "ดำเนินการต่อ"; + +/* chat item action */ +"Copy" = "คัดลอก"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "รุ่นหลัก: v%@"; + +/* No comment provided by engineer. */ +"Create" = "สร้าง"; + +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ"; + +/* server test step */ +"Create file" = "สร้างไฟล์"; + +/* No comment provided by engineer. */ +"Create group link" = "สร้างลิงค์กลุ่ม"; + +/* No comment provided by engineer. */ +"Create link" = "สร้างลิงค์"; + +/* No comment provided by engineer. */ +"Create one-time invitation link" = "สร้างลิงก์เชิญแบบใช้ครั้งเดียว"; + +/* server test step */ +"Create queue" = "สร้างคิว"; + +/* No comment provided by engineer. */ +"Create secret group" = "สร้างกลุ่มลับ"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "สร้างที่อยู่ SimpleX"; + +/* No comment provided by engineer. */ +"Create your profile" = "สร้างโปรไฟล์ของคุณ"; + +/* No comment provided by engineer. */ +"Created on %@" = "สร้างเมื่อ %@"; + +/* No comment provided by engineer. */ +"creator" = "ผู้สร้าง"; + +/* No comment provided by engineer. */ +"Current Passcode" = "รหัสผ่านปัจจุบัน"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "รหัสผ่านปัจจุบัน…"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@"; + +/* dropdown time picker choice */ +"custom" = "กำหนดเอง"; + +/* No comment provided by engineer. */ +"Custom time" = "เวลาที่กําหนดเอง"; + +/* No comment provided by engineer. */ +"Dark" = "มืด"; + +/* No comment provided by engineer. */ +"Database downgrade" = "ดาวน์เกรดฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "ฐานข้อมูลถูก encrypt แล้ว!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "รหัสผ่านแบบ encrypt ที่ใช้ในการเข้าฐานข้อมูลจะได้รับการอัปเดตและจัดเก็บไว้ใน keychain\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "รหัสผ่าน encryption ของฐานข้อมูลจะได้รับการอัปเดต\n"; + +/* No comment provided by engineer. */ +"Database error" = "ฐานข้อมูลผิดพลาด"; + +/* No comment provided by engineer. */ +"Database ID" = "ID ฐานข้อมูล"; + +/* copied message info */ +"Database ID: %d" = "ID ฐานข้อมูล: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "ID ฐานข้อมูลและตัวเลือกการแยกการส่งผ่าน"; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม คุณสามารถเปลี่ยนได้"; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม โปรดเปลี่ยนก่อนส่งออก"; + +/* No comment provided by engineer. */ +"Database passphrase" = "รหัสผ่านของฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "รหัสผ่านฐานข้อมูล & ส่งออก"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "รหัสผ่านของฐานข้อมูลแตกต่างจากที่บันทึกไว้ใน keychain"; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "ต้องใช้รหัสผ่านของฐานข้อมูลในการเปิดแชท"; + +/* No comment provided by engineer. */ +"Database upgrade" = "อัพเกรดฐานข้อมูล"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลลงสำหรับ: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "ฐานข้อมูลจะถูก encrypt และรหัสผ่านจะถูกจัดเก็บไว้ใน keychain\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "ฐานข้อมูลจะถูก encrypt\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "ระบบจะย้ายฐานข้อมูลเมื่อแอปรีสตาร์ท"; + +/* time unit */ +"days" = "วัน"; + +/* No comment provided by engineer. */ +"Decentralized" = "กระจายอำนาจแล้ว"; + +/* message decrypt error item */ +"Decryption error" = "ข้อผิดพลาดในการ decrypt"; + +/* pref value */ +"default (%@)" = "ค่าเริ่มต้น (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "ค่าเริ่มต้น (ไม่)"; + +/* No comment provided by engineer. */ +"default (yes)" = "ค่าเริ่มต้น (ใช่)"; + +/* chat item action */ +"Delete" = "ลบ"; + +/* No comment provided by engineer. */ +"Delete address" = "ลบที่อยู่"; + +/* No comment provided by engineer. */ +"Delete address?" = "ลบที่อยู่?"; + +/* No comment provided by engineer. */ +"Delete after" = "ลบหลังจาก"; + +/* No comment provided by engineer. */ +"Delete all files" = "ลบไฟล์ทั้งหมด"; + +/* No comment provided by engineer. */ +"Delete archive" = "ลบที่เก็บถาวร"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "ลบที่เก็บแชทถาวร?"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "ลบโปรไฟล์แชท"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "ลบโปรไฟล์แชทไหม?"; + +/* No comment provided by engineer. */ +"Delete connection" = "ลบการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Delete contact" = "ลบผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Delete Contact" = "ลบผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Delete contact?" = "ลบผู้ติดต่อ?"; + +/* No comment provided by engineer. */ +"Delete database" = "ลบฐานข้อมูล"; + +/* server test step */ +"Delete file" = "ลบไฟล์"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "ลบไฟล์และสื่อ?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "ลบไฟล์สําหรับโปรไฟล์แชททั้งหมด"; + +/* chat feature */ +"Delete for everyone" = "ลบสำหรับทุกคน"; + +/* No comment provided by engineer. */ +"Delete for me" = "ลบให้ฉัน"; + +/* No comment provided by engineer. */ +"Delete group" = "ลบกลุ่ม"; + +/* No comment provided by engineer. */ +"Delete group?" = "ลบกลุ่ม?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "ลบคำเชิญ"; + +/* No comment provided by engineer. */ +"Delete link" = "ลบลิงค์"; + +/* No comment provided by engineer. */ +"Delete link?" = "ลบลิงค์ ไหม?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "ลบข้อความสมาชิก?"; + +/* No comment provided by engineer. */ +"Delete message?" = "ลบข้อความ?"; + +/* No comment provided by engineer. */ +"Delete messages" = "ลบข้อความ"; + +/* No comment provided by engineer. */ +"Delete messages after" = "ลบข้อความหลังจาก"; + +/* No comment provided by engineer. */ +"Delete old database" = "ลบฐานข้อมูลเก่า"; + +/* No comment provided by engineer. */ +"Delete old database?" = "ลบฐานข้อมูลเก่า?"; + +/* No comment provided by engineer. */ +"Delete pending connection" = "ลบการเชื่อมต่อที่รอดำเนินการ"; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?"; + +/* No comment provided by engineer. */ +"Delete profile" = "ลบโปรไฟล์"; + +/* server test step */ +"Delete queue" = "ลบคิว"; + +/* No comment provided by engineer. */ +"Delete user profile?" = "ลบโปรไฟล์ผู้ใช้?"; + +/* deleted chat item */ +"deleted" = "ลบแล้ว"; + +/* No comment provided by engineer. */ +"Deleted at" = "ลบที่"; + +/* copied message info */ +"Deleted at: %@" = "ลบที่: %@"; + +/* rcv group event chat item */ +"deleted group" = "กลุ่มที่ถูกลบ"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "ใบตอบรับการจัดส่งถูกปิดใช้งาน!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "ใบตอบรับการจัดส่ง!"; + +/* No comment provided by engineer. */ +"Description" = "คำอธิบาย"; + +/* No comment provided by engineer. */ +"Develop" = "พัฒนา"; + +/* No comment provided by engineer. */ +"Developer tools" = "เครื่องมือสำหรับนักพัฒนา"; + +/* No comment provided by engineer. */ +"Device" = "อุปกรณ์"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "การตรวจสอบสิทธิ์อุปกรณ์ถูกปิดใช้งาน กำลังปิด SimpleX Lock"; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "การตรวจสอบอุปกรณ์ไม่ได้ถูกเปิดใช้งาน คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่าได้ เมื่อคุณเปิดใช้งานการตรวจสอบอุปกรณ์แล้ว"; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "การโยกย้ายที่แตกต่างกันในแอป/ฐานข้อมูล: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "ชื่ออวตารและการแยกการขนส่งที่แตกต่างกัน"; + +/* connection level description */ +"direct" = "โดยตรง"; + +/* chat feature */ +"Direct messages" = "ข้อความโดยตรง"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this group." = "ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้"; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "ปิดใช้งาน (เก็บการแทนที่)"; + +/* No comment provided by engineer. */ +"Disable for all" = "ปิดการใช้งานสำหรับทุกคน"; + +/* authentication reason */ +"Disable SimpleX Lock" = "ปิดการใช้งาน SimpleX Lock"; + +/* No comment provided by engineer. */ +"Disappearing message" = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message)"; + +/* chat feature */ +"Disappearing messages" = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message)"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในแชทนี้"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this group." = "ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) เป็นสิ่งต้องห้ามในกลุ่มนี้"; + +/* No comment provided by engineer. */ +"Disappears at" = "หายไปที่"; + +/* copied message info */ +"Disappears at: %@" = "หายไปที่: %@"; + +/* server test step */ +"Disconnect" = "ตัดการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Display name" = "ชื่อที่แสดง"; + +/* No comment provided by engineer. */ +"Display name:" = "ชื่อที่แสดง:"; + +/* No comment provided by engineer. */ +"Do it later" = "ทำในภายหลัง"; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน"; + +/* No comment provided by engineer. */ +"Don't create address" = "อย่าสร้างที่อยู่"; + +/* No comment provided by engineer. */ +"Don't enable" = "อย่าเปิดใช้งาน"; + +/* No comment provided by engineer. */ +"Don't show again" = "ไม่ต้องแสดงอีก"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "ปรับลดรุ่นและเปิดแชท"; + +/* server test step */ +"Download file" = "ดาวน์โหลดไฟล์"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "ชื่อที่แสดงซ้ำ!"; + +/* integrity error chat item */ +"duplicate message" = "ข้อความที่ซ้ำกัน"; + +/* No comment provided by engineer. */ +"Duration" = "ระยะเวลา"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "encrypted จากต้นจนจบ"; + +/* chat item action */ +"Edit" = "แก้ไข"; + +/* No comment provided by engineer. */ +"Edit group profile" = "แก้ไขโปรไฟล์กลุ่ม"; + +/* No comment provided by engineer. */ +"Enable" = "เปิดใช้งาน"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "เปิดใช้งาน (เก็บการแทนที่)"; + +/* No comment provided by engineer. */ +"Enable automatic message deletion?" = "เปิดใช้งานการลบข้อความอัตโนมัติ?"; + +/* No comment provided by engineer. */ +"Enable for all" = "เปิดใช้งานสําหรับทุกคน"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "เปิดใช้งานการแจ้งเตือนทันที?"; + +/* No comment provided by engineer. */ +"Enable lock" = "เปิดใช้งานการล็อค"; + +/* No comment provided by engineer. */ +"Enable notifications" = "เปิดใช้งานการแจ้งเตือน"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "เปิดใช้การแจ้งเตือนเป็นระยะๆ ไหม?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "เปิดใช้งานการทำลายตัวเอง"; + +/* set passcode view */ +"Enable self-destruct passcode" = "เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง"; + +/* authentication reason */ +"Enable SimpleX Lock" = "เปิดใช้งาน SimpleX Lock"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "เปิดใช้งาน TCP Keep-alive"; + +/* enabled status */ +"enabled" = "เปิดใช้งาน"; + +/* enabled status */ +"enabled for contact" = "ได้เปิดใช้งานสำหรับการติดต่อแล้ว"; + +/* enabled status */ +"enabled for you" = "เปิดใช้งานสําหรับคุณแล้ว"; + +/* No comment provided by engineer. */ +"Encrypt" = "Encrypt"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Encrypt ฐานข้อมูล?"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Encrypt ฐานข้อมูลเรียบร้อยแล้ว"; + +/* notification */ +"Encrypted message or another event" = "ข้อความที่ encrypt หรือเหตุการณ์อื่น"; + +/* notification */ +"Encrypted message: database error" = "ข้อความที่ encrypt: ความผิดพลาดในฐานข้อมูล"; + +/* notification */ +"Encrypted message: database migration error" = "ข้อความที่ encrypt: ข้อผิดพลาดในการย้ายฐานข้อมูล"; + +/* notification */ +"Encrypted message: keychain error" = "ข้อความที่ encrypt: ข้อผิดพลาดของ keychain"; + +/* notification */ +"Encrypted message: no passphrase" = "ข้อความที่ encrypt: ไม่มีรหัสผ่าน"; + +/* notification */ +"Encrypted message: unexpected error" = "ข้อความที่ encrypt: ข้อผิดพลาดที่ไม่คาดคิด"; + +/* chat item text */ +"encryption agreed" = "ตกลง encryption"; + +/* chat item text */ +"encryption agreed for %@" = "ตกลง encryption สําหรับ % @"; + +/* chat item text */ +"encryption ok" = "encryptionใช้ได้"; + +/* chat item text */ +"encryption ok for %@" = "encryptionใช้ได้สําหรับ %@"; + +/* chat item text */ +"encryption re-negotiation allowed" = "อนุญาตให้มีการเจรจา encryption อีกครั้ง"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "อนุญาตให้มีการเจรจา encryption อีกครั้งสําหรับ %@"; + +/* chat item text */ +"encryption re-negotiation required" = "จำเป็นต้องมีการเจรจา encryption อีกครั้ง"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "จำเป็นต้องมีการเจรจา encryption อีกครั้งสําหรับ %@"; + +/* No comment provided by engineer. */ +"ended" = "สิ้นสุดลงแล้ว"; + +/* call status */ +"ended call %@" = "สิ้นสุดการโทร %@"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "ใส่รหัสผ่านที่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Enter Passcode" = "ใส่รหัสผ่าน"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "ใส่รหัสผ่าน"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "ใส่รหัสผ่านด้านบนเพื่อแสดง!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "ใส่เซิร์ฟเวอร์ด้วยตนเอง"; + +/* placeholder */ +"Enter welcome message…" = "ใส่ข้อความต้อนรับ…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "ใส่ข้อความต้อนรับ… (ไม่บังคับ)"; + +/* No comment provided by engineer. */ +"error" = "ผิดพลาด"; + +/* No comment provided by engineer. */ +"Error" = "ผิดพลาด"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "เกิดข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "เกิดข้อผิดพลาดในการรับคำขอติดต่อ"; + +/* No comment provided by engineer. */ +"Error accessing database file" = "เกิดข้อผิดพลาดในการเข้าถึงไฟล์ฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "เกิดข้อผิดพลาดในการเพิ่มสมาชิก"; + +/* No comment provided by engineer. */ +"Error changing address" = "เกิดข้อผิดพลาดในการเปลี่ยนที่อยู่"; + +/* No comment provided by engineer. */ +"Error changing role" = "เกิดข้อผิดพลาดในการเปลี่ยนบทบาท"; + +/* No comment provided by engineer. */ +"Error changing setting" = "เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า"; + +/* No comment provided by engineer. */ +"Error creating address" = "เกิดข้อผิดพลาดในการสร้างที่อยู่"; + +/* No comment provided by engineer. */ +"Error creating group" = "เกิดข้อผิดพลาดในการสร้างกลุ่ม"; + +/* No comment provided by engineer. */ +"Error creating group link" = "เกิดข้อผิดพลาดในการสร้างลิงก์กลุ่ม"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "เกิดข้อผิดพลาดในการสร้างโปรไฟล์!"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลแชท"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "เกิดข้อผิดพลาดในการลบแชท!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "เกิดข้อผิดพลาดในการลบการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Error deleting contact" = "เกิดข้อผิดพลาดในการลบผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Error deleting database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูลเก่า"; + +/* No comment provided by engineer. */ +"Error deleting token" = "เกิดข้อผิดพลาดในการลบโทเค็น"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "เกิดข้อผิดพลาดในการลบโปรไฟล์ผู้ใช้"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "เกิดข้อผิดพลาดในการเปิดใช้ใบเสร็จการจัดส่ง!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "เกิดข้อผิดพลาดในการเปิดใช้งานการแจ้งเตือน"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "เกิดข้อผิดพลาดในการ encrypt ฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท"; + +/* No comment provided by engineer. */ +"Error joining group" = "เกิดข้อผิดพลาดในการเข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"Error loading %@ servers" = "โหลดเซิร์ฟเวอร์ %@ ผิดพลาด"; + +/* No comment provided by engineer. */ +"Error receiving file" = "เกิดข้อผิดพลาดในการรับไฟล์"; + +/* No comment provided by engineer. */ +"Error removing member" = "เกิดข้อผิดพลาดในการลบสมาชิก"; + +/* No comment provided by engineer. */ +"Error saving %@ servers" = "เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "เกิดข้อผิดพลาดในการบันทึกโปรไฟล์กลุ่ม"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ ICE"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "เกิดข้อผิดพลาดในการบันทึกรหัสผ่าน"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "เกิดข้อผิดพลาดในการบันทึกรหัสผ่านไปยัง keychain"; + +/* No comment provided by engineer. */ +"Error saving user password" = "เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้"; + +/* No comment provided by engineer. */ +"Error sending email" = "เกิดข้อผิดพลาดในการส่งอีเมล"; + +/* No comment provided by engineer. */ +"Error sending message" = "เกิดข้อผิดพลาดในการส่งข้อความ"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "เกิดข้อผิดพลาดในการตั้งค่าใบตอบรับการจัดส่ง!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "เกิดข้อผิดพลาดในการเริ่มแชท"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "เกิดข้อผิดพลาดในการหยุดแชท"; + +/* No comment provided by engineer. */ +"Error switching profile!" = "เกิดข้อผิดพลาดในการเปลี่ยนโปรไฟล์!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "เกิดข้อผิดพลาดในการซิงโครไนซ์การเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Error updating group link" = "เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม"; + +/* No comment provided by engineer. */ +"Error updating message" = "เกิดข้อผิดพลาดในการอัปเดตข้อความ"; + +/* No comment provided by engineer. */ +"Error updating settings" = "เกิดข้อผิดพลาดในการอัปเดตการตั้งค่า"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "เกิดข้อผิดพลาดในการอัปเดตข้อมูลส่วนตัวของผู้ใช้"; + +/* No comment provided by engineer. */ +"Error: " = "ผิดพลาด: "; + +/* No comment provided by engineer. */ +"Error: %@" = "ข้อผิดพลาด: % @"; + +/* No comment provided by engineer. */ +"Error: no database file" = "เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "เกิดข้อผิดพลาด: URL ไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "แม้ในขณะที่ปิดใช้งานในการสนทนา"; + +/* No comment provided by engineer. */ +"Exit without saving" = "ออกโดยไม่บันทึก"; + +/* No comment provided by engineer. */ +"Export database" = "ส่งออกฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Export error:" = "ข้อผิดพลาดในการส่งออก:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "ที่เก็บถาวรฐานข้อมูลที่ส่งออก"; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "กำลังส่งออกที่เก็บถาวรฐานข้อมูล…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "ไม่สามารถลบรหัสผ่านได้"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!"; + +/* No comment provided by engineer. */ +"Favorite" = "ที่ชอบ"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "จะได้รับไฟล์เมื่อผู้ติดต่อของคุณอัปโหลดเสร็จ"; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "จะได้รับไฟล์เมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!"; + +/* No comment provided by engineer. */ +"File: %@" = "ไฟล์: % @"; + +/* No comment provided by engineer. */ +"Files & media" = "ไฟล์และสื่อ"; + +/* chat feature */ +"Files and media" = "ไฟล์และสื่อ"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "ไฟล์และสื่อเป็นสิ่งต้องห้ามในกลุ่มนี้"; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "ไฟล์และสื่อต้องห้าม!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "กรองแชทที่ยังไม่อ่านและแชทโปรด"; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "ในที่สุดเราก็มีแล้ว! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "ค้นหาแชทได้เร็วขึ้น"; + +/* No comment provided by engineer. */ +"Fix" = "แก้ไข"; + +/* No comment provided by engineer. */ +"Fix connection" = "แก้ไขการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Fix connection?" = "แก้ไขการเชื่อมต่อ?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "แก้ไข encryption หลังจากกู้คืนข้อมูลสำรอง"; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "การแก้ไขไม่รองรับโดยผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม"; + +/* No comment provided by engineer. */ +"For console" = "สำหรับคอนโซล"; + +/* No comment provided by engineer. */ +"French interface" = "อินเทอร์เฟซภาษาฝรั่งเศส"; + +/* No comment provided by engineer. */ +"Full link" = "ลิงค์เต็ม"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "ชื่อเต็ม (ไม่บังคับ)"; + +/* No comment provided by engineer. */ +"Full name:" = "ชื่อเต็ม:"; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "ดำเนินการใหม่อย่างสมบูรณ์ - ทำงานในพื้นหลัง!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "ลดการใช้แบตเตอรี่เพิ่มเติม"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIFs และสติกเกอร์"; + +/* No comment provided by engineer. */ +"Group" = "กลุ่ม"; + +/* No comment provided by engineer. */ +"group deleted" = "ลบกลุ่มแล้ว"; + +/* No comment provided by engineer. */ +"Group display name" = "ชื่อกลุ่มที่แสดง"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "ชื่อเต็มกลุ่ม (ไม่บังคับ)"; + +/* No comment provided by engineer. */ +"Group image" = "ภาพกลุ่ม"; + +/* No comment provided by engineer. */ +"Group invitation" = "คําเชิญเข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "คำเชิญเข้าร่วมกลุ่มหมดอายุแล้ว"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "คำเชิญเข้าร่วมกลุ่มใช้ไม่ถูกต้องอีกต่อไป คำเชิญถูกลบโดยผู้ส่ง"; + +/* No comment provided by engineer. */ +"Group link" = "ลิงค์กลุ่ม"; + +/* No comment provided by engineer. */ +"Group links" = "ลิงค์กลุ่ม"; + +/* No comment provided by engineer. */ +"Group members can add message reactions." = "สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้"; + +/* No comment provided by engineer. */ +"Group members can irreversibly delete sent messages." = "สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร"; + +/* No comment provided by engineer. */ +"Group members can send direct messages." = "สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้"; + +/* No comment provided by engineer. */ +"Group members can send disappearing messages." = "สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; + +/* No comment provided by engineer. */ +"Group members can send files and media." = "สมาชิกกลุ่มสามารถส่งไฟล์และสื่อ"; + +/* No comment provided by engineer. */ +"Group members can send voice messages." = "สมาชิกกลุ่มสามารถส่งข้อความเสียง"; + +/* notification */ +"Group message:" = "ข้อความกลุ่ม:"; + +/* No comment provided by engineer. */ +"Group moderation" = "การกลั่นกรองกลุ่ม"; + +/* No comment provided by engineer. */ +"Group preferences" = "ค่ากําหนดลักษณะกลุ่ม"; + +/* No comment provided by engineer. */ +"Group profile" = "โปรไฟล์กลุ่ม"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "โปรไฟล์กลุ่มถูกจัดเก็บไว้ในอุปกรณ์ของสมาชิก ไม่ใช่บนเซิร์ฟเวอร์"; + +/* snd group event chat item */ +"group profile updated" = "อัปเดตโปรไฟล์กลุ่มแล้ว"; + +/* No comment provided by engineer. */ +"Group welcome message" = "ข้อความต้อนรับกลุ่ม"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "กลุ่มจะถูกลบสำหรับสมาชิกทั้งหมด - ไม่สามารถยกเลิกได้!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "กลุ่มจะถูกลบสำหรับคุณ - ไม่สามารถยกเลิกได้!"; + +/* No comment provided by engineer. */ +"Help" = "ความช่วยเหลือ"; + +/* No comment provided by engineer. */ +"Hidden" = "ซ่อนอยู่"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "โปรไฟล์การแชทที่ซ่อนอยู่"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "รหัสผ่านโปรไฟล์ที่ซ่อนอยู่"; + +/* chat item action */ +"Hide" = "ซ่อน"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "ซ่อนหน้าจอแอพในแอพล่าสุด"; + +/* No comment provided by engineer. */ +"Hide profile" = "ซ่อนโปรไฟล์"; + +/* No comment provided by engineer. */ +"Hide:" = "ซ่อน:"; + +/* No comment provided by engineer. */ +"History" = "ประวัติ"; + +/* time unit */ +"hours" = "ชั่วโมง"; + +/* No comment provided by engineer. */ +"How it works" = "มันทำงานอย่างไร"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "วิธีการ SimpleX ทํางานอย่างไร"; + +/* No comment provided by engineer. */ +"How to" = "วิธี"; + +/* No comment provided by engineer. */ +"How to use it" = "วิธีการใช้งาน"; + +/* No comment provided by engineer. */ +"How to use your servers" = "วิธีใช้เซิร์ฟเวอร์ของคุณ"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "เซิร์ฟเวอร์ ICE (หนึ่งเครื่องต่อสาย)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์"; + +/* No comment provided by engineer. */ +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "หากคุณไม่สามารถพบปะด้วยตนเอง คุณสามารถ **สแกนคิวอาร์โค้ดผ่านการสนทนาทางวิดีโอ** หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้"; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "หากคุณใส่รหัสผ่านทำลายตัวเองขณะเปิดแอป:"; + +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "หากคุณจำเป็นต้องใช้แชทตอนนี้ ให้แตะ **ทำในภายหลัง** ด้านล่าง (ระบบจะเสนอให้คุณย้ายฐานข้อมูลเมื่อคุณรีสตาร์ทแอป)"; + +/* No comment provided by engineer. */ +"Ignore" = "ไม่สนใจ"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "จะได้รับภาพเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จสิ้น"; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "จะได้รับรูปภาพเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!"; + +/* No comment provided by engineer. */ +"Immediately" = "โดยทันที"; + +/* No comment provided by engineer. */ +"Immune to spam and abuse" = "มีภูมิคุ้มกันต่อสแปมและการละเมิด"; + +/* No comment provided by engineer. */ +"Import" = "นำเข้า"; + +/* No comment provided by engineer. */ +"Import chat database?" = "นำเข้าฐานข้อมูลแชท?"; + +/* No comment provided by engineer. */ +"Import database" = "นำเข้าฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "ปรับปรุงความเป็นส่วนตัวและความปลอดภัยแล้ว"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "ปรับปรุงการกําหนดค่าเซิร์ฟเวอร์แล้ว"; + +/* No comment provided by engineer. */ +"In reply to" = "ในการตอบกลับถึง"; + +/* No comment provided by engineer. */ +"Incognito" = "ไม่ระบุตัวตน"; + +/* No comment provided by engineer. */ +"Incognito mode" = "โหมดไม่ระบุตัวตน"; + +/* chat list item description */ +"incognito via contact address link" = "ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ"; + +/* chat list item description */ +"incognito via group link" = "ไม่ระบุตัวตนผ่านลิงก์กลุ่ม"; + +/* chat list item description */ +"incognito via one-time link" = "ไม่ระบุตัวตนผ่านลิงก์แบบใช้ครั้งเดียว"; + +/* notification */ +"Incoming audio call" = "สายเรียกเข้า"; + +/* notification */ +"Incoming call" = "สายเรียกเข้า"; + +/* notification */ +"Incoming video call" = "สายวิดีโอเข้ามา"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้"; + +/* PIN entry */ +"Incorrect passcode" = "รหัสผ่านไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "รหัสความปลอดภัยไม่ถูกต้อง!"; + +/* connection level description */ +"indirect (%d)" = "ทางอ้อม (%d)"; + +/* chat item action */ +"Info" = "ข้อมูล"; + +/* No comment provided by engineer. */ +"Initial role" = "บทบาทเริ่มต้น"; + +/* No comment provided by engineer. */ +"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "ติดตั้ง [SimpleX Chat สำหรับเทอร์มินัล](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "การแจ้งเตือนโดยทันทีจะถูกซ่อน!\n"; + +/* No comment provided by engineer. */ +"Instantly" = "ทันที"; + +/* No comment provided by engineer. */ +"Interface" = "อินเตอร์เฟซ"; + +/* invalid chat data */ +"invalid chat" = "แชทไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"invalid chat data" = "ข้อมูลแชทไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "ลิงค์เชื่อมต่อไม่ถูกต้อง"; + +/* invalid chat item */ +"invalid data" = "ข้อมูลไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Invalid server address!" = "ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "คำเชิญหมดอายุแล้ว!"; + +/* group name */ +"invitation to group %@" = "คำเชิญเข้าร่วมกลุ่ม %@"; + +/* No comment provided by engineer. */ +"Invite friends" = "เชิญเพื่อนๆ"; + +/* No comment provided by engineer. */ +"Invite members" = "เชิญสมาชิก"; + +/* No comment provided by engineer. */ +"Invite to group" = "เชิญเข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"invited" = "เชิญ"; + +/* rcv group event chat item */ +"invited %@" = "เชิญ %@"; + +/* chat list item title */ +"invited to connect" = "ได้รับเชิญให้เชื่อมต่อ"; + +/* rcv group event chat item */ +"invited via your group link" = "ถูกเชิญผ่านลิงค์กลุ่มของคุณ"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain ใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัย - อนุญาตให้รับการแจ้งเตือนแบบทันที"; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "iOS Keychain จะใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัยหลังจากที่คุณรีสตาร์ทแอปหรือเปลี่ยนรหัสผ่าน ซึ่งจะช่วยให้รับการแจ้งเตือนแบบทันทีได้"; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "การลบข้อความแบบแก้ไขไม่ได้"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this group." = "การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้"; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "อนุญาตให้มีการเชื่อมต่อที่ไม่ระบุตัวตนจำนวนมากโดยไม่มีข้อมูลที่ใช้ร่วมกันระหว่างกันในโปรไฟล์การแชทเดียว"; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "สามารถเกิดขึ้นได้เมื่อคุณหรือการเชื่อมต่อของคุณใช้การสํารองข้อมูลฐานข้อมูลเก่า"; + +/* No comment provided by engineer. */ +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "มันสามารถเกิดขึ้นได้เมื่อ:\n1. ข้อความหมดอายุในไคลเอนต์ที่ส่งหลังจาก 2 วันหรือบนเซิร์ฟเวอร์หลังจาก 30 วัน\n2. การถอดรหัสข้อความล้มเหลว เนื่องจากคุณหรือผู้ติดต่อของคุณใช้การสำรองฐานข้อมูลเก่า\n3. การเชื่อมต่อถูกบุกรุก"; + +/* No comment provided by engineer. */ +"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "อินเทอร์เฟซภาษาอิตาลี"; + +/* No comment provided by engineer. */ +"italic" = "ตัวเอียง"; + +/* No comment provided by engineer. */ +"Japanese interface" = "อินเทอร์เฟซภาษาญี่ปุ่น"; + +/* No comment provided by engineer. */ +"Join" = "เข้าร่วม"; + +/* No comment provided by engineer. */ +"join as %@" = "เข้าร่วมเป็น %@"; + +/* No comment provided by engineer. */ +"Join group" = "เข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"Join incognito" = "เข้าร่วมแบบไม่ระบุตัวตน"; + +/* No comment provided by engineer. */ +"Joining group" = "กำลังจะเข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"Keep your connections" = "รักษาการเชื่อมต่อของคุณ"; + +/* No comment provided by engineer. */ +"Keychain error" = "ข้อผิดพลาดของ Keychain"; + +/* No comment provided by engineer. */ +"KeyChain error" = "ข้อผิดพลาดของ Keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "ไฟล์ขนาดใหญ่!"; + +/* No comment provided by engineer. */ +"Learn more" = "ศึกษาเพิ่มเติม"; + +/* No comment provided by engineer. */ +"Leave" = "ออกจาก"; + +/* No comment provided by engineer. */ +"Leave group" = "ออกจากกลุ่ม"; + +/* No comment provided by engineer. */ +"Leave group?" = "ออกจากกลุ่ม?"; + +/* rcv group event chat item */ +"left" = "ออกแล้ว"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "มาคุยกันใน SimpleX Chat"; + +/* No comment provided by engineer. */ +"Light" = "สว่าง"; + +/* No comment provided by engineer. */ +"Limitations" = "ข้อจำกัด"; + +/* No comment provided by engineer. */ +"LIVE" = "สด"; + +/* No comment provided by engineer. */ +"Live message!" = "ข้อความสด!"; + +/* No comment provided by engineer. */ +"Live messages" = "ข้อความสด"; + +/* No comment provided by engineer. */ +"Local name" = "ชื่อภายในเครื่องเท่านั้น"; + +/* No comment provided by engineer. */ +"Local profile data only" = "ข้อมูลโปรไฟล์ภายในเครื่องเท่านั้น"; + +/* No comment provided by engineer. */ +"Lock after" = "ล็อคหลังจาก"; + +/* No comment provided by engineer. */ +"Lock mode" = "โหมดล็อค"; + +/* No comment provided by engineer. */ +"Make a private connection" = "สร้างการเชื่อมต่อแบบส่วนตัว"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "ทำให้ข้อความหายไปหนึ่งข้อความ"; + +/* No comment provided by engineer. */ +"Make profile private!" = "ทำให้โปรไฟล์เป็นส่วนตัว!"; + +/* No comment provided by engineer. */ +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ %@ อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน (%@)"; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน"; + +/* No comment provided by engineer. */ +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "หลายคนถามว่า: *หาก SimpleX ไม่มีตัวระบุผู้ใช้ จะส่งข้อความได้อย่างไร?*"; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "ทำเครื่องหมายว่าลบแล้วสำหรับทุกคน"; + +/* No comment provided by engineer. */ +"Mark read" = "ทำเครื่องหมายอ่านแล้ว"; + +/* No comment provided by engineer. */ +"Mark verified" = "ทําเครื่องหมายว่ายืนยันแล้ว"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Markdown ในข้อความ"; + +/* marked deleted chat item preview text */ +"marked deleted" = "ทำเครื่องหมายว่าลบแล้ว"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "สูงสุด 30 วินาที รับทันที"; + +/* member role */ +"member" = "สมาชิก"; + +/* No comment provided by engineer. */ +"Member" = "สมาชิก"; + +/* rcv group event chat item */ +"member connected" = "เชื่อมต่อสำเร็จ"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "บทบาทของสมาชิกจะถูกเปลี่ยนเป็น \"%@\" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "บทบาทของสมาชิกจะถูกเปลี่ยนเป็น \"%@\" สมาชิกจะได้รับคำเชิญใหม่"; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้!"; + +/* item status text */ +"Message delivery error" = "ข้อผิดพลาดในการส่งข้อความ"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "ใบเสร็จการส่งข้อความ!"; + +/* No comment provided by engineer. */ +"Message draft" = "ร่างข้อความ"; + +/* chat feature */ +"Message reactions" = "ปฏิกิริยาของข้อความ"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้"; + +/* notification */ +"message received" = "ข้อความที่ได้รับ"; + +/* No comment provided by engineer. */ +"Message text" = "ข้อความ"; + +/* No comment provided by engineer. */ +"Messages" = "ข้อความ"; + +/* No comment provided by engineer. */ +"Messages & files" = "ข้อความและไฟล์"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "กำลังย้ายข้อมูลที่เก็บถาวรของฐานข้อมูล…"; + +/* No comment provided by engineer. */ +"Migration error:" = "ข้อผิดพลาดในการย้ายข้อมูล:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "การย้ายข้อมูลล้มเหลว แตะ **ข้าม** ด้านล่างเพื่อใช้ฐานข้อมูลปัจจุบันต่อไป โปรดรายงานปัญหาให้นักพัฒนาแอปทราบผ่านทางแชทหรืออีเมล [chat@simplex.chat](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"Migration is completed" = "การโยกย้ายเสร็จสมบูรณ์"; + +/* No comment provided by engineer. */ +"Migrations: %@" = "การย้ายข้อมูล: %@"; + +/* time unit */ +"minutes" = "นาที"; + +/* call status */ +"missed call" = "สายที่ไม่ได้รับ"; + +/* chat item action */ +"Moderate" = "กลั่นกรอง"; + +/* moderated chat item */ +"moderated" = "กลั่นกรองแล้ว"; + +/* No comment provided by engineer. */ +"Moderated at" = "กลั่นกรองที่"; + +/* copied message info */ +"Moderated at: %@" = "กลั่นกรองที่: %@"; + +/* No comment provided by engineer. */ +"moderated by %@" = "กลั่นกรองโดย %@"; + +/* time unit */ +"months" = "เดือน"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!"; + +/* No comment provided by engineer. */ +"Most likely this contact has deleted the connection with you." = "เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ"; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "โปรไฟล์การแชทหลายรายการ"; + +/* No comment provided by engineer. */ +"Mute" = "ปิดเสียง"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "ปิดเสียงเมื่อไม่ได้ใช้งาน!"; + +/* No comment provided by engineer. */ +"Name" = "ชื่อ"; + +/* No comment provided by engineer. */ +"Network & servers" = "เครือข่ายและเซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"Network settings" = "การตั้งค่าเครือข่าย"; + +/* No comment provided by engineer. */ +"Network status" = "สถานะเครือข่าย"; + +/* No comment provided by engineer. */ +"never" = "ไม่เคย"; + +/* notification */ +"New contact request" = "คำขอติดต่อใหม่"; + +/* notification */ +"New contact:" = "คำขอติดต่อใหม่:"; + +/* No comment provided by engineer. */ +"New database archive" = "ฐานข้อมูลใหม่สำหรับการเก็บถาวร"; + +/* No comment provided by engineer. */ +"New display name" = "ชื่อที่แสดงใหม่"; + +/* No comment provided by engineer. */ +"New in %@" = "ใหม่ใน %@"; + +/* No comment provided by engineer. */ +"New member role" = "บทบาทของสมาชิกใหม่"; + +/* notification */ +"new message" = "ข้อความใหม่"; + +/* notification */ +"New message" = "ข้อความใหม่"; + +/* No comment provided by engineer. */ +"New Passcode" = "รหัสผ่านใหม่"; + +/* No comment provided by engineer. */ +"New passphrase…" = "รหัสผ่านใหม่…"; + +/* pref value */ +"no" = "ไม่"; + +/* No comment provided by engineer. */ +"No" = "เลขที่"; + +/* Authentication unavailable */ +"No app password" = "ไม่มีรหัสผ่านสำหรับแอป"; + +/* No comment provided by engineer. */ +"No contacts selected" = "ไม่ได้เลือกผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"No contacts to add" = "ไม่มีรายชื่อที่จะเพิ่ม"; + +/* No comment provided by engineer. */ +"No device token!" = "ไม่มีโทเค็นอุปกรณ์!"; + +/* No comment provided by engineer. */ +"no e2e encryption" = "ไม่มีการ encrypt จากต้นจนจบ"; + +/* No comment provided by engineer. */ +"No filtered chats" = "ไม่มีการกรองการแชท"; + +/* No comment provided by engineer. */ +"No group!" = "ไม่พบกลุ่ม!"; + +/* No comment provided by engineer. */ +"No history" = "ไม่มีประวัติ"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "ไม่อนุญาตให้บันทึกข้อความเสียง"; + +/* No comment provided by engineer. */ +"No received or sent files" = "ไม่มีไฟล์ที่ได้รับหรือส่ง"; + +/* copied message info in history */ +"no text" = "ไม่มีข้อความ"; + +/* No comment provided by engineer. */ +"Notifications" = "การแจ้งเตือน"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "ปิดการแจ้งเตือน!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "ขณะนี้ผู้ดูแลระบบสามารถ:\n- ลบข้อความของสมาชิก\n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\")"; + +/* member role */ +"observer" = "ผู้สังเกตการณ์"; + +/* enabled status + group pref value */ +"off" = "ปิด"; + +/* No comment provided by engineer. */ +"Off" = "ปิด"; + +/* No comment provided by engineer. */ +"Off (Local)" = "ปิด (ในเครื่อง)"; + +/* feature offered item */ +"offered %@" = "เสนอแล้ว %@"; + +/* feature offered item */ +"offered %@: %@" = "เสนอแล้ว %1$@: %2$@"; + +/* No comment provided by engineer. */ +"Ok" = "ตกลง"; + +/* No comment provided by engineer. */ +"Old database" = "ฐานข้อมูลเก่า"; + +/* No comment provided by engineer. */ +"Old database archive" = "คลังฐานข้อมูลเก่า"; + +/* group pref value */ +"on" = "เปิด"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "ลิงก์คำเชิญแบบใช้ครั้งเดียว"; + +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "โฮสต์หัวหอมจะไม่ถูกใช้"; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น**"; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้"; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานไฟล์และสื่อได้"; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้"; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้"; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)"; + +/* No comment provided by engineer. */ +"Only you can make calls." = "มีเพียงคุณเท่านั้นที่โทรออกได้"; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้"; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้"; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)"; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "ผู้ติดต่อของคุณเท่านั้นที่สามารถโทรออกได้"; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้"; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "ผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความเสียงได้"; + +/* No comment provided by engineer. */ +"Open chat" = "เปิดแชท"; + +/* authentication reason */ +"Open chat console" = "เปิดคอนโซลการแชท"; + +/* No comment provided by engineer. */ +"Open Settings" = "เปิดการตั้งค่า"; + +/* authentication reason */ +"Open user profiles" = "เปิดโปรไฟล์ผู้ใช้"; + +/* No comment provided by engineer. */ +"Open-source protocol and code – anybody can run the servers." = "โปรโตคอลและโค้ดโอเพ่นซอร์ส – ใคร ๆ ก็สามารถเปิดใช้เซิร์ฟเวอร์ได้"; + +/* No comment provided by engineer. */ +"Opening database…" = "กำลังเปิดฐานข้อมูล…"; + +/* No comment provided by engineer. */ +"Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "การเปิดลิงก์ในเบราว์เซอร์อาจลดความเป็นส่วนตัวและความปลอดภัยของการเชื่อมต่อ ลิงก์ SimpleX ที่ไม่น่าเชื่อถือจะเป็นสีแดง"; + +/* No comment provided by engineer. */ +"or chat with the developers" = "หรือแชทกับนักพัฒนาแอป"; + +/* member role */ +"owner" = "เจ้าของ"; + +/* No comment provided by engineer. */ +"Passcode" = "รหัสผ่าน"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "เปลี่ยนรหัสผ่านแล้ว!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "การใส่รหัสผ่าน"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "รหัสผ่านไม่เปลี่ยน!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "ตั้งรหัสผ่านเรียบร้อยแล้ว!"; + +/* No comment provided by engineer. */ +"Password to show" = "รหัสผ่านที่จะแสดง"; + +/* No comment provided by engineer. */ +"Paste" = "แปะ"; + +/* No comment provided by engineer. */ +"Paste image" = "แปะภาพ"; + +/* No comment provided by engineer. */ +"Paste received link" = "แปะลิงก์ที่ได้รับ"; + +/* No comment provided by engineer. */ +"peer-to-peer" = "เพื่อนต่อเพื่อน"; + +/* No comment provided by engineer. */ +"People can connect to you only via the links you share." = "ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น"; + +/* No comment provided by engineer. */ +"Periodically" = "เป็นระยะๆ"; + +/* message decrypt error item */ +"Permanent decryption error" = "ข้อผิดพลาดในการถอดรหัสอย่างถาวร"; + +/* No comment provided by engineer. */ +"PING count" = "จํานวน PING"; + +/* No comment provided by engineer. */ +"PING interval" = "ช่วงเวลา PING"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "โปรดขอให้ผู้ติดต่อของคุณเปิดใช้งานการส่งข้อความเสียง"; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ"; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "โปรดตรวจสอบการเชื่อมต่อเครือข่ายของคุณกับ %@ แล้วลองอีกครั้ง"; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "โปรดตรวจสอบความต้องการของคุณและการตั้งค่าผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Please contact group admin." = "โปรดติดต่อผู้ดูแลกลุ่ม"; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "โปรดใส่รหัสผ่านปัจจุบันที่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "โปรดใส่รหัสผ่านก่อนหน้านี้หลังจากกู้คืนข้อมูลสํารองฐานข้อมูล การกระทํานี้ไม่สามารถยกเลิกได้"; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "โปรดจำหรือเก็บไว้อย่างปลอดภัย - ไม่มีทางกู้คืนรหัสผ่านที่หายไปได้!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "โปรดรายงานไปยังผู้พัฒนาแอป"; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "โปรดรีสตาร์ทแอปและย้ายฐานข้อมูลเพื่อเปิดใช้งานการแจ้งเตือนแบบทันที"; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "โปรดเก็บรหัสผ่านไว้อย่างปลอดภัย คุณจะไม่สามารถเข้าถึงแชทได้หากทำหาย"; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "โปรดจัดเก็บรหัสผ่านอย่างปลอดภัย คุณจะไม่สามารถเปลี่ยนรหัสผ่านได้หากคุณทำรหัสผ่านหาย"; + +/* No comment provided by engineer. */ +"Polish interface" = "อินเตอร์เฟซภาษาโปแลนด์"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "อาจเป็นไปได้ว่าลายนิ้วมือของ certificate ในที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ"; + +/* No comment provided by engineer. */ +"Preset server" = "เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; + +/* No comment provided by engineer. */ +"Preset server address" = "ที่อยู่เซิร์ฟเวอร์ที่ตั้งไว้ล่วงหน้า"; + +/* No comment provided by engineer. */ +"Preview" = "ดูตัวอย่าง"; + +/* No comment provided by engineer. */ +"Privacy & security" = "ความเป็นส่วนตัวและความปลอดภัย"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "นิยามความเป็นส่วนตัวใหม่"; + +/* No comment provided by engineer. */ +"Private filenames" = "ชื่อไฟล์ส่วนตัว"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"Profile image" = "รูปโปรไฟล์"; + +/* No comment provided by engineer. */ +"Profile password" = "รหัสผ่านโปรไฟล์"; + +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ"; + +/* No comment provided by engineer. */ +"Prohibit audio/video calls." = "ห้ามการโทรด้วยเสียง/วิดีโอ"; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "ห้ามการลบข้อความที่ย้อนกลับไม่ได้"; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "ห้ามแสดงปฏิกิริยาต่อข้อความ"; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "ห้ามแสดงปฏิกิริยาต่อข้อความ"; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "ห้ามส่งข้อความโดยตรงถึงสมาชิก"; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "ห้ามส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages)"; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "ห้ามส่งไฟล์และสื่อ"; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "ห้ามส่งข้อความเสียง"; + +/* No comment provided by engineer. */ +"Protect app screen" = "ปกป้องหน้าจอแอป"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน!"; + +/* No comment provided by engineer. */ +"Protocol timeout" = "หมดเวลาโปรโตคอล"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "การหมดเวลาของโปรโตคอลต่อ KB"; + +/* No comment provided by engineer. */ +"Push notifications" = "การแจ้งเตือนแบบทันที"; + +/* No comment provided by engineer. */ +"Rate the app" = "ให้คะแนนแอป"; + +/* chat item menu */ +"React…" = "ตอบสนอง…"; + +/* No comment provided by engineer. */ +"Read" = "อ่าน"; + +/* No comment provided by engineer. */ +"Read more" = "อ่านเพิ่มเติม"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "อ่านเพิ่มเติมใน[คู่มือผู้ใช้](https://simplex.chat/docs/guide/readme.html#connect-to-friends)"; + +/* No comment provided by engineer. */ +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "อ่านเพิ่มเติมใน[พื้นที่เก็บข้อมูล GitHub](https://github.com/simplex-chat/simplex-chat#readme)"; + +/* No comment provided by engineer. */ +"Read more in our GitHub repository." = "อ่านเพิ่มเติมในที่เก็บ GitHub ของเรา"; + +/* No comment provided by engineer. */ +"received answer…" = "ได้รับคำตอบ…"; + +/* No comment provided by engineer. */ +"Received at" = "ได้รับเมื่อ"; + +/* copied message info */ +"Received at: %@" = "ได้รับเมื่อ: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "ได้รับการยืนยัน…"; + +/* notification */ +"Received file event" = "ได้รับไฟล์"; + +/* message info title */ +"Received message" = "ได้รับข้อความ"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์"; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "การรับไฟล์จะหยุดลง"; + +/* No comment provided by engineer. */ +"Receiving via" = "กำลังรับผ่าน"; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์"; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "เชื่อมต่อเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดอีกครั้งเพื่อบังคับให้ส่งข้อความ มันใช้การจราจรเพิ่มเติม"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "เชื่อมต่อเซิร์ฟเวอร์อีกครั้งหรือไม่?"; + +/* No comment provided by engineer. */ +"Record updated at" = "บันทึกถูกอัปเดตเมื่อ"; + +/* copied message info */ +"Record updated at: %@" = "บันทึกถูกอัปเดตเมื่อ: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "ลดการใช้แบตเตอรี่"; + +/* reject incoming call via notification */ +"Reject" = "ปฏิเสธ"; + +/* No comment provided by engineer. */ +"Reject contact request" = "ปฏิเสธคำขอติดต่อ"; + +/* call status */ +"rejected call" = "สายถูกปฏิเสธ"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "ใช้เซิร์ฟเวอร์รีเลย์ในกรณีที่จำเป็นเท่านั้น บุคคลอื่นสามารถสังเกตที่อยู่ IP ของคุณได้"; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "เซิร์ฟเวอร์รีเลย์ปกป้องที่อยู่ IP ของคุณ แต่สามารถสังเกตระยะเวลาของการโทรได้"; + +/* No comment provided by engineer. */ +"Remove" = "ลบ"; + +/* No comment provided by engineer. */ +"Remove member" = "ลบสมาชิกออก"; + +/* No comment provided by engineer. */ +"Remove member?" = "ลบสมาชิกออก?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "ลบรหัสผ่านออกจาก keychain หรือไม่?"; + +/* No comment provided by engineer. */ +"removed" = "ถูกลบแล้ว"; + +/* rcv group event chat item */ +"removed %@" = "ถูกลบแล้ว %@"; + +/* rcv group event chat item */ +"removed you" = "ลบคุณออกแล้ว"; + +/* No comment provided by engineer. */ +"Renegotiate" = "เจรจาใหม่"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "เจรจา encryption อีกครั้ง"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "เจรจา enryption ใหม่หรือไม่?"; + +/* chat item action */ +"Reply" = "ตอบ"; + +/* No comment provided by engineer. */ +"Required" = "ที่จำเป็น"; + +/* No comment provided by engineer. */ +"Reset" = "รีเซ็ต"; + +/* No comment provided by engineer. */ +"Reset colors" = "รีเซ็ตสี"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "รีเซ็ตเป็นค่าเริ่มต้น"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "รีสตาร์ทแอปเพื่อใช้ฐานข้อมูลการแชทที่นำเข้า"; + +/* No comment provided by engineer. */ +"Restore" = "คืนค่า"; + +/* No comment provided by engineer. */ +"Restore database backup" = "คืนค่าการสำรองฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "คืนค่าฐานข้อมูลสำรองไหม?"; + +/* No comment provided by engineer. */ +"Restore database error" = "กู้คืนข้อผิดพลาดของฐานข้อมูล"; + +/* chat item action */ +"Reveal" = "เปิดเผย"; + +/* No comment provided by engineer. */ +"Revert" = "เปลี่ยนกลับ"; + +/* No comment provided by engineer. */ +"Revoke" = "ถอน"; + +/* cancel file action */ +"Revoke file" = "ถอนไฟล์"; + +/* No comment provided by engineer. */ +"Revoke file?" = "ถอนไฟล์?"; + +/* No comment provided by engineer. */ +"Role" = "บทบาท"; + +/* No comment provided by engineer. */ +"Run chat" = "เรียกใช้แชท"; + +/* chat item action */ +"Save" = "บันทึก"; + +/* No comment provided by engineer. */ +"Save (and notify contacts)" = "บันทึก (และแจ้งผู้ติดต่อ)"; + +/* No comment provided by engineer. */ +"Save and notify contact" = "บันทึกและแจ้งผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "บันทึกและอัปเดตโปรไฟล์กลุ่ม"; + +/* No comment provided by engineer. */ +"Save archive" = "บันทึกไฟล์เก็บถาวร"; + +/* No comment provided by engineer. */ +"Save auto-accept settings" = "บันทึกการตั้งค่าการยอมรับอัตโนมัติ"; + +/* No comment provided by engineer. */ +"Save group profile" = "บันทึกโปรไฟล์กลุ่ม"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "บันทึกรหัสผ่านและเปิดแชท"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "บันทึกข้อความรหัสผ่านใน Keychain"; + +/* No comment provided by engineer. */ +"Save preferences?" = "บันทึกการตั้งค่า?"; + +/* No comment provided by engineer. */ +"Save profile password" = "บันทึกรหัสผ่านโปรไฟล์"; + +/* No comment provided by engineer. */ +"Save servers" = "บันทึกเซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"Save servers?" = "บันทึกเซิร์ฟเวอร์?"; + +/* No comment provided by engineer. */ +"Save settings?" = "บันทึกการตั้งค่า?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "บันทึกข้อความต้อนรับ?"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก"; + +/* No comment provided by engineer. */ +"Scan code" = "สแกนรหัส"; + +/* No comment provided by engineer. */ +"Scan QR code" = "สแกนคิวอาร์โค้ด"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "สแกนรหัสความปลอดภัยจากแอปของผู้ติดต่อของคุณ"; + +/* No comment provided by engineer. */ +"Scan server QR code" = "สแกนคิวอาร์โค้ดของเซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"Search" = "ค้นหา"; + +/* network option */ +"sec" = "วินาที"; + +/* time unit */ +"seconds" = "วินาที"; + +/* No comment provided by engineer. */ +"secret" = "ความลับ"; + +/* server test step */ +"Secure queue" = "คิวที่ปลอดภัย"; + +/* No comment provided by engineer. */ +"Security assessment" = "การประเมินความปลอดภัย"; + +/* No comment provided by engineer. */ +"Security code" = "รหัสความปลอดภัย"; + +/* chat item text */ +"security code changed" = "เปลี่ยนรหัสความปลอดภัยแล้ว"; + +/* No comment provided by engineer. */ +"Select" = "เลือก"; + +/* No comment provided by engineer. */ +"Self-destruct" = "ทําลายตัวเอง"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "รหัสผ่านแบบทําลายตัวเอง"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "รหัสผ่านแบบทําลายตัวเองเปลี่ยนไป!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "เปิดใช้งานรหัสผ่านแบบทําลายตัวเองแล้ว!"; + +/* No comment provided by engineer. */ +"Send" = "ส่ง"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "ส่งข้อความสด - มันจะอัปเดตสําหรับผู้รับในขณะที่คุณพิมพ์"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "ส่งใบเสร็จรับการจัดส่งข้อความไปที่"; + +/* No comment provided by engineer. */ +"Send direct message" = "ส่งข้อความโดยตรง"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "ส่งข้อความแบบที่หายไป"; + +/* No comment provided by engineer. */ +"Send link previews" = "ส่งตัวอย่างลิงก์"; + +/* No comment provided by engineer. */ +"Send live message" = "ส่งข้อความสด"; + +/* No comment provided by engineer. */ +"Send notifications" = "ส่งการแจ้งเตือน"; + +/* No comment provided by engineer. */ +"Send notifications:" = "ส่งการแจ้งเตือน:"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "ส่งคําถามและความคิด"; + +/* No comment provided by engineer. */ +"Send receipts" = "ส่งใบเสร็จ"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "ส่งจากแกลเลอรีหรือแป้นพิมพ์แบบกำหนดเอง"; + +/* No comment provided by engineer. */ +"Sender cancelled file transfer." = "ผู้ส่งยกเลิกการโอนไฟล์"; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว"; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิดในโปรไฟล์แชทที่มองเห็นได้ทั้งหมด"; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิด"; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "การส่งไฟล์จะหยุดลง"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "การส่งใบเสร็จถูกปิดใช้งานสำหรับผู้ติดต่อ %lld"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "การส่งใบเสร็จถูกเปิดใช้งานสำหรับผู้ติดต่อ %lld"; + +/* No comment provided by engineer. */ +"Sending via" = "ส่งผ่าน"; + +/* No comment provided by engineer. */ +"Sent at" = "ส่งเมื่อ"; + +/* copied message info */ +"Sent at: %@" = "ส่งเมื่อ: %@"; + +/* notification */ +"Sent file event" = "เหตุการณ์ไฟล์ที่ส่ง"; + +/* message info title */ +"Sent message" = "ข้อความที่ส่งแล้ว"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด"; + +/* server test error */ +"Server requires authorization to create queues, check password" = "เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน"; + +/* server test error */ +"Server requires authorization to upload, check password" = "เซิร์ฟเวอร์ต้องการการอนุญาตในการอัปโหลด โปรดตรวจสอบรหัสผ่าน"; + +/* No comment provided by engineer. */ +"Server test failed!" = "การทดสอบเซิร์ฟเวอร์ล้มเหลว!"; + +/* No comment provided by engineer. */ +"Servers" = "เซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"Set 1 day" = "ตั้ง 1 วัน"; + +/* No comment provided by engineer. */ +"Set contact name…" = "ตั้งชื่อผู้ติดต่อ…"; + +/* No comment provided by engineer. */ +"Set group preferences" = "ตั้งค่าการกําหนดลักษณะกลุ่ม"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "ตั้งแทนการรับรองความถูกต้องของระบบ"; + +/* No comment provided by engineer. */ +"Set passcode" = "ตั้งรหัสผ่าน"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "ตั้งรหัสผ่านเพื่อส่งออก"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "ตั้งข้อความที่แสดงต่อสมาชิกใหม่!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "ตั้งค่าการหมดเวลาสำหรับพร็อกซี/VPN"; + +/* No comment provided by engineer. */ +"Settings" = "การตั้งค่า"; + +/* chat item action */ +"Share" = "แชร์"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "แชร์ลิงก์แบบใช้ครั้งเดียว"; + +/* No comment provided by engineer. */ +"Share address" = "แชร์ที่อยู่"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "แชร์ที่อยู่กับผู้ติดต่อ?"; + +/* No comment provided by engineer. */ +"Share link" = "แชร์ลิงก์"; + +/* No comment provided by engineer. */ +"Share one-time invitation link" = "แชร์ลิงก์เชิญแบบใช้ครั้งเดียว"; + +/* No comment provided by engineer. */ +"Share with contacts" = "แชร์กับผู้ติดต่อ"; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "แสดงการโทรในประวัติการโทร"; + +/* No comment provided by engineer. */ +"Show developer options" = "แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์"; + +/* No comment provided by engineer. */ +"Show preview" = "แสดงตัวอย่าง"; + +/* No comment provided by engineer. */ +"Show:" = "แสดง:"; + +/* No comment provided by engineer. */ +"SimpleX address" = "ที่อยู่ SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "ที่อยู่ SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "ความปลอดภัยของ SimpleX Chat ได้รับการตรวจสอบโดย Trail of Bits"; + +/* simplex link type */ +"SimpleX contact address" = "ที่อยู่ติดต่อ SimpleX"; + +/* notification */ +"SimpleX encrypted message or connection event" = "ข้อความ encrypt ของ SimpleX หรือเหตุการณ์การเชื่อมต่อ"; + +/* simplex link type */ +"SimpleX group link" = "ลิงค์กลุ่ม SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX links" = "ลิงก์ SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "ล็อค SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "โหมดล็อค SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "ไม่ได้เปิดใช้งาน SimpleX Lock!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX Lock เปิดอยู่"; + +/* simplex link type */ +"SimpleX one-time invitation" = "คำเชิญ SimpleX แบบครั้งเดียว"; + +/* No comment provided by engineer. */ +"Skip" = "ข้าม"; + +/* No comment provided by engineer. */ +"Skipped messages" = "ข้อความที่ข้ามไป"; + +/* No comment provided by engineer. */ +"SMP servers" = "เซิร์ฟเวอร์ SMP"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat"; + +/* notification title */ +"Somebody" = "ใครบางคน"; + +/* No comment provided by engineer. */ +"Start a new chat" = "เริ่มแชทใหม่"; + +/* No comment provided by engineer. */ +"Start chat" = "เริ่มแชท"; + +/* No comment provided by engineer. */ +"Start migration" = "เริ่มการย้ายข้อมูล"; + +/* No comment provided by engineer. */ +"starting…" = "กำลังเริ่มต้น…"; + +/* No comment provided by engineer. */ +"Stop" = "หยุด"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง"; + +/* No comment provided by engineer. */ +"Stop chat?" = "หยุดแชท?"; + +/* cancel file action */ +"Stop file" = "หยุดไฟล์"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "หยุดรับไฟล์?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "หยุดส่งไฟล์ไหม?"; + +/* No comment provided by engineer. */ +"Stop sharing" = "หยุดแชร์"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "หยุดแชร์ที่อยู่ไหม?"; + +/* authentication reason */ +"Stop SimpleX" = "หยุด SimpleX"; + +/* No comment provided by engineer. */ +"strike" = "ตี"; + +/* No comment provided by engineer. */ +"Submit" = "ส่ง"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "สนับสนุน SimpleX แชท"; + +/* No comment provided by engineer. */ +"System" = "ระบบ"; + +/* No comment provided by engineer. */ +"System authentication" = "การรับรองความถูกต้องของระบบ"; + +/* No comment provided by engineer. */ +"Take picture" = "ถ่ายภาพ"; + +/* No comment provided by engineer. */ +"Tap button " = "แตะปุ่ม "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "แตะเพื่อเปิดใช้งานโปรไฟล์"; + +/* No comment provided by engineer. */ +"Tap to join" = "แตะเพื่อเข้าร่วม"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน"; + +/* No comment provided by engineer. */ +"Tap to start a new chat" = "แตะเพื่อเริ่มแชทใหม่"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "หมดเวลาการเชื่อมต่อ TCP"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* server test failure */ +"Test failed at step %@." = "การทดสอบล้มเหลวในขั้นตอน %@"; + +/* No comment provided by engineer. */ +"Test server" = "เซิร์ฟเวอร์ทดสอบ"; + +/* No comment provided by engineer. */ +"Test servers" = "เซิร์ฟเวอร์ทดสอบ"; + +/* No comment provided by engineer. */ +"Tests failed!" = "การทดสอบล้มเหลว!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "ขอบคุณสำหรับการติดตั้ง SimpleX Chat!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "ขอบคุณผู้ใช้ – [มีส่วนร่วมผ่าน Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!"; + +/* No comment provided by engineer. */ +"The 1st platform without any user identifiers – private by design." = "แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว"; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน"; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์"; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "ผู้ติดต่อที่คุณแชร์ลิงก์นี้ด้วยจะไม่สามารถเชื่อมต่อได้!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "ไฟล์เก็บที่สร้างขึ้นมีอยู่ในการตั้งค่าแอพ / ฐานข้อมูล / คลังฐานข้อมูลเก่า"; + +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!"; + +/* No comment provided by engineer. */ +"The group is fully decentralized – it is visible only to the members." = "กลุ่มมีการกระจายอำนาจอย่างเต็มที่ – มองเห็นได้เฉพาะสมาชิกเท่านั้น"; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "แฮชของข้อความก่อนหน้านี้แตกต่างกัน"; + +/* No comment provided by engineer. */ +"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "ID ของข้อความถัดไปไม่ถูกต้อง\n(น้อยกว่าหรือเท่ากับข้อความก่อนหน้า)\nอาจเกิดขึ้นได้เนื่องจากข้อบกพร่องบางอย่างหรือเมื่อการเชื่อมต่อถูกบุกรุก"; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "ข้อความจะถูกลบสำหรับสมาชิกทั้งหมด"; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน"; + +/* No comment provided by engineer. */ +"The next generation of private messaging" = "การส่งข้อความส่วนตัวรุ่นต่อไป"; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "ฐานข้อมูลเก่าไม่ได้ถูกลบในระหว่างการย้ายข้อมูล แต่สามารถลบได้"; + +/* No comment provided by engineer. */ +"The profile is only shared with your contacts." = "โปรไฟล์นี้แชร์กับผู้ติดต่อของคุณเท่านั้น"; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "ขีดที่สองที่เราพลาด! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "ผู้ส่งจะไม่ได้รับแจ้ง"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**"; + +/* No comment provided by engineer. */ +"Theme" = "ธีม"; + +/* No comment provided by engineer. */ +"There should be at least one user profile." = "ควรมีโปรไฟล์ผู้ใช้อย่างน้อยหนึ่งโปรไฟล์"; + +/* No comment provided by engineer. */ +"There should be at least one visible user profile." = "ควรมีอย่างน้อยหนึ่งโปรไฟล์ผู้ใช้ที่มองเห็นได้"; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@**"; + +/* No comment provided by engineer. */ +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่"; + +/* No comment provided by engineer. */ +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "การดำเนินการนี้ไม่สามารถเลิกทำได้ - ข้อความที่ส่งและรับก่อนหน้าที่เลือกไว้จะถูกลบ อาจใช้เวลาหลายนาที"; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร"; + +/* notification title */ +"this contact" = "ผู้ติดต่อนี้"; + +/* No comment provided by engineer. */ +"This group no longer exists." = "ไม่มีกลุ่มนี้แล้ว"; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@**"; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "หากต้องการถามคำถามและรับการอัปเดต:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "เพื่อการเชื่อมต่อ ผู้ติดต่อของคุณสามารถสแกนคิวอาร์โค้ดหรือใช้ลิงก์ในแอป"; + +/* No comment provided by engineer. */ +"To make a new connection" = "เพื่อสร้างการเชื่อมต่อใหม่"; + +/* No comment provided by engineer. */ +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย"; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา"; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "เพื่อปกป้องข้อมูลของคุณ ให้เปิด SimpleX Lock\nคุณจะได้รับแจ้งให้ยืนยันตัวตนให้เสร็จสมบูรณ์ก่อนที่จะเปิดใช้งานคุณลักษณะนี้"; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "ในการบันทึกข้อความเสียง โปรดให้สิทธิ์ในการใช้ไมโครโฟน"; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้า **โปรไฟล์แชทของคุณ**"; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "เพื่อรองรับการแจ้งเตือนแบบทันที ฐานข้อมูลการแชทจะต้องได้รับการโยกย้าย"; + +/* No comment provided by engineer. */ +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ"; + +/* No comment provided by engineer. */ +"Transport isolation" = "การแยกการขนส่ง"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@)"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "พยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้"; + +/* No comment provided by engineer. */ +"Turn off" = "ปิด"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "ปิดการแจ้งเตือนไหม?"; + +/* No comment provided by engineer. */ +"Turn on" = "เปิด"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "ไม่สามารถบันทึกข้อความเสียง"; + +/* item status description */ +"Unexpected error: %@" = "ข้อผิดพลาดที่ไม่คาดคิด: %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "สถานะการย้ายข้อมูลที่ไม่คาดคิด"; + +/* No comment provided by engineer. */ +"Unfav." = "เลิกชอบ"; + +/* No comment provided by engineer. */ +"Unhide" = "ยกเลิกการซ่อน"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "ยกเลิกการซ่อนโปรไฟล์การแชท"; + +/* No comment provided by engineer. */ +"Unhide profile" = "เลิกซ่อนโปรไฟล์"; + +/* No comment provided by engineer. */ +"Unit" = "หน่วย"; + +/* connection info */ +"unknown" = "ไม่ทราบ"; + +/* callkit banner */ +"Unknown caller" = "ผู้โทรที่ไม่รู้จัก"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "ข้อผิดพลาดของฐานข้อมูลที่ไม่รู้จัก: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "ข้อผิดพลาดที่ไม่รู้จัก"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน"; + +/* No comment provided by engineer. */ +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "เว้นแต่ผู้ติดต่อของคุณลบการเชื่อมต่อหรือลิงก์นี้ถูกใช้ไปแล้ว อาจเป็นข้อผิดพลาด โปรดรายงาน\nในการเชื่อมต่อ โปรดขอให้ผู้ติดต่อของคุณสร้างลิงก์การเชื่อมต่ออื่น และตรวจสอบว่าคุณมีการเชื่อมต่อเครือข่ายที่เสถียร"; + +/* No comment provided by engineer. */ +"Unlock" = "ปลดล็อค"; + +/* authentication reason */ +"Unlock app" = "ปลดล็อคแอป"; + +/* No comment provided by engineer. */ +"Unmute" = "เปิดเสียง"; + +/* No comment provided by engineer. */ +"Unread" = "เปลี่ยนเป็นยังไม่ได้อ่าน"; + +/* No comment provided by engineer. */ +"Update" = "อัปเดต"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "อัปเดตการตั้งค่าโฮสต์ .onion ไหม?"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "อัปเดตรหัสผ่านของฐานข้อมูล"; + +/* No comment provided by engineer. */ +"Update network settings?" = "อัปเดตการตั้งค่าเครือข่ายไหม?"; + +/* No comment provided by engineer. */ +"Update transport isolation mode?" = "อัปเดตโหมดการแยกการขนส่งไหม?"; + +/* rcv group event chat item */ +"updated group profile" = "อัปเดตโปรไฟล์กลุ่มแล้ว"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง"; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง"; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "อัปเกรดและเปิดการแชท"; + +/* server test step */ +"Upload file" = "อัปโหลดไฟล์"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "ใช้โฮสต์ .onion"; + +/* No comment provided by engineer. */ +"Use chat" = "ใช้แชท"; + +/* No comment provided by engineer. */ +"Use for new connections" = "ใช้สำหรับการเชื่อมต่อใหม่"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "ใช้อินเทอร์เฟซการโทร iOS"; + +/* No comment provided by engineer. */ +"Use server" = "ใช้เซิร์ฟเวอร์"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "ใช้เซิร์ฟเวอร์ SimpleX Chat ไหม?"; + +/* No comment provided by engineer. */ +"User profile" = "โปรไฟล์ผู้ใช้"; + +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = "การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้"; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่"; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify connection security" = "ตรวจสอบความปลอดภัยในการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"Verify security code" = "ตรวจสอบรหัสความปลอดภัย"; + +/* No comment provided by engineer. */ +"Via browser" = "ผ่านเบราว์เซอร์"; + +/* chat list item description */ +"via contact address link" = "ผ่านลิงค์ที่อยู่ติดต่อ"; + +/* chat list item description */ +"via group link" = "ผ่านลิงค์กลุ่ม"; + +/* chat list item description */ +"via one-time link" = "ผ่านลิงค์แบบใช้ครั้งเดียว"; + +/* No comment provided by engineer. */ +"via relay" = "ผ่านรีเลย์"; + +/* No comment provided by engineer. */ +"Video call" = "การสนทนาทางวิดีโอ"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "การสนทนาทางวิดีโอ (ไม่ได้ encrypt จากต้นจนจบ)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จ"; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "จะได้รับวิดีโอเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "วิดีโอและไฟล์สูงสุด 1gb"; + +/* No comment provided by engineer. */ +"View security code" = "ดูรหัสความปลอดภัย"; + +/* No comment provided by engineer. */ +"Voice message…" = "ข้อความเสียง…"; + +/* chat feature */ +"Voice messages" = "ข้อความเสียง"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "ห้ามส่งข้อความเสียงในแชทนี้"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this group." = "ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้"; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "ห้ามข้อความเสียง!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "รอคำตอบ…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "รอการยืนยัน…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "กำลังรอไฟล์"; + +/* No comment provided by engineer. */ +"Waiting for image" = "กําลังรอภาพ"; + +/* No comment provided by engineer. */ +"Waiting for video" = "กําลังรอวิดีโอ"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "ต้องการเชื่อมต่อกับคุณ!"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "คำเตือน: คุณอาจสูญเสียข้อมูลบางส่วน!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "เซิร์ฟเวอร์ WebRTC ICE"; + +/* time unit */ +"weeks" = "สัปดาห์"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "ยินดีต้อนรับ %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "ข้อความต้อนรับ"; + +/* No comment provided by engineer. */ +"What's new" = "มีอะไรใหม่"; + +/* No comment provided by engineer. */ +"When available" = "เมื่อพร้อมใช้งาน"; + +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้"; + +/* No comment provided by engineer. */ +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "เมื่อคุณแชร์โปรไฟล์ที่ไม่ระบุตัวตนกับใครสักคน โปรไฟล์นี้จะใช้สำหรับกลุ่มที่พวกเขาเชิญคุณ"; + +/* No comment provided by engineer. */ +"With optional welcome message." = "พร้อมข้อความต้อนรับที่ไม่บังคับ"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "รหัสผ่านฐานข้อมูลไม่ถูกต้อง"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "รหัสผ่านผิด!"; + +/* No comment provided by engineer. */ +"XFTP servers" = "เซิร์ฟเวอร์ XFTP"; + +/* pref value */ +"yes" = "ใช่"; + +/* No comment provided by engineer. */ +"You" = "คุณ"; + +/* No comment provided by engineer. */ +"You accepted connection" = "คุณยอมรับการเชื่อมต่อ"; + +/* No comment provided by engineer. */ +"You allow" = "คุณอนุญาต"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "คุณมีโปรไฟล์แชทที่ใช้ชื่อแสดงเดียวกันอยู่แล้ว กรุณาเลือกชื่ออื่น"; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "คุณได้เชื่อมต่อกับ %@ แล้ว"; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "คุณเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้"; + +/* No comment provided by engineer. */ +"you are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"You are invited to group" = "คุณได้รับเชิญให้เข้าร่วมกลุ่ม"; + +/* No comment provided by engineer. */ +"you are observer" = "คุณเป็นผู้สังเกตการณ์"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป"; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม **เปิดในแอปมือถือ**"; + +/* No comment provided by engineer. */ +"You can create it later" = "คุณสามารถสร้างได้ในภายหลัง"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป"; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา"; + +/* notification body */ +"You can now send messages to %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า"; + +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง"; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ **%@**"; + +/* No comment provided by engineer. */ +"You can share your address as a link or QR code - anybody can connect to you." = "คุณสามารถแชร์ที่อยู่ของคุณเป็นลิงก์หรือรหัสคิวอาร์ - ใคร ๆ ก็สามารถเชื่อมต่อกับคุณได้"; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป"; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า"; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "คุณสามารถใช้มาร์กดาวน์เพื่อจัดรูปแบบข้อความ:"; + +/* No comment provided by engineer. */ +"You can't send messages!" = "คุณไม่สามารถส่งข้อความได้!"; + +/* chat item text */ +"you changed address" = "คุณเปลี่ยนที่อยู่แล้ว"; + +/* chat item text */ +"you changed address for %@" = "คุณเปลี่ยนที่อยู่สำหรับ %@"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "คุณเปลี่ยนบทบาทสำหรับตัวคุณเองเป็น %@"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "คุณเปลี่ยนบทบาทของ %1$@ เป็น %2$@"; + +/* No comment provided by engineer. */ +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "คุณควบคุมผ่านเซิร์ฟเวอร์ **เพื่อรับ** ข้อความผู้ติดต่อของคุณ - เซิร์ฟเวอร์ที่คุณใช้เพื่อส่งข้อความถึงพวกเขา"; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง."; + +/* No comment provided by engineer. */ +"You have no chats" = "คุณไม่มีการแชท"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์"; + +/* No comment provided by engineer. */ +"You joined this group" = "คุณเข้าร่วมกลุ่มนี้แล้ว"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "คุณเข้าร่วมกลุ่มนี้แล้ว กำลังเชื่อมต่อเพื่อเชิญสมาชิกกลุ่ม"; + +/* snd group event chat item */ +"you left" = "คุณออกไปแล้ว"; + +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "คุณต้องใช้ฐานข้อมูลแชทเวอร์ชันล่าสุดบนอุปกรณ์เครื่องเดียวเท่านั้น มิฉะนั้น คุณอาจหยุดได้รับข้อความจากผู้ติดต่อบางคน"; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "คุณต้องอนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียงจึงจะสามารถส่งได้"; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "คุณปฏิเสธคำเชิญเข้าร่วมกลุ่ม"; + +/* snd group event chat item */ +"you removed %@" = "คุณลบ %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "คุณส่งคำเชิญเข้าร่วมกลุ่มแล้ว"; + +/* chat list item description */ +"you shared one-time link" = "คุณแชร์ลิงก์แบบใช้ครั้งเดียวแล้ว"; + +/* chat list item description */ +"you shared one-time link incognito" = "คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตนแล้ว"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "คุณจะเชื่อมต่อกับกลุ่มเมื่ออุปกรณ์โฮสต์ของกลุ่มออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "คุณจะเชื่อมต่อเมื่อคำขอเชื่อมต่อของคุณได้รับการยอมรับ โปรดรอหรือตรวจสอบในภายหลัง!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "คุณจะเชื่อมต่อเมื่ออุปกรณ์ของผู้ติดต่อของคุณออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง"; + +/* No comment provided by engineer. */ +"You will join a group this link refers to and connect to its group members." = "คุณจะเข้าร่วมกลุ่มที่ลิงก์นี้อ้างถึงและเชื่อมต่อกับสมาชิกในกลุ่ม"; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "คุณจะยังได้รับสายเรียกเข้าและการแจ้งเตือนจากโปรไฟล์ที่ปิดเสียงเมื่อโปรไฟล์ของเขามีการใช้งาน"; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "คุณจะหยุดได้รับข้อความจากกลุ่มนี้ ประวัติการแชทจะถูกรักษาไว้"; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "คุณจะไม่สูญเสียรายชื่อผู้ติดต่อของคุณหากคุณลบที่อยู่ในภายหลัง"; + +/* No comment provided by engineer. */ +"you: " = "คุณ: "; + +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "คุณกำลังพยายามเชิญผู้ติดต่อที่คุณแบ่งปันโปรไฟล์ที่ไม่ระบุตัวตนไปยังกลุ่มที่คุณใช้โปรไฟล์หลักของคุณ"; + +/* No comment provided by engineer. */ +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "คุณกำลังใช้โปรไฟล์ที่ไม่ระบุตัวตนสำหรับกลุ่มนี้ - ไม่อนุญาตให้เชิญผู้ติดต่อเพื่อป้องกันการแชร์โปรไฟล์หลักของคุณ"; + +/* No comment provided by engineer. */ +"Your %@ servers" = "เซิร์ฟเวอร์ %@ ของคุณ"; + +/* No comment provided by engineer. */ +"Your calls" = "การโทรของคุณ"; + +/* No comment provided by engineer. */ +"Your chat database" = "ฐานข้อมูลการแชทของคุณ"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "ฐานข้อมูลการแชทของคุณไม่ได้ถูก encrypt - ตั้งรหัสผ่านเพื่อ encrypt"; + +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "โปรไฟล์แชทของคุณ"; + +/* No comment provided by engineer. */ +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์\nคุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่)"; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)"; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้"; + +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "ผู้ติดต่อของคุณใน SimpleX จะเห็น\nคุณสามารถเปลี่ยนได้ในการตั้งค่า"; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่"; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "ฐานข้อมูลแชทปัจจุบันของคุณจะถูกลบและแทนที่ด้วยฐานข้อมูลที่นำเข้า"; + +/* No comment provided by engineer. */ +"Your current profile" = "โปรไฟล์ปัจจุบันของคุณ"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "เซิร์ฟเวอร์ ICE ของคุณ"; + +/* No comment provided by engineer. */ +"Your preferences" = "การตั้งค่าของคุณ"; + +/* No comment provided by engineer. */ +"Your privacy" = "ความเป็นส่วนตัวของคุณ"; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "โปรไฟล์ของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณและแชร์กับผู้ติดต่อของคุณเท่านั้น\nเซิร์ฟเวอร์ SimpleX ไม่สามารถดูโปรไฟล์ของคุณได้"; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "โปรไฟล์ รายชื่อผู้ติดต่อ และข้อความที่ส่งของคุณจะถูกจัดเก็บไว้ในอุปกรณ์ของคุณ"; + +/* No comment provided by engineer. */ +"Your random profile" = "โปรไฟล์แบบสุ่มของคุณ"; + +/* No comment provided by engineer. */ +"Your server" = "เซิร์ฟเวอร์ของคุณ"; + +/* No comment provided by engineer. */ +"Your server address" = "ที่อยู่เซิร์ฟเวอร์ของคุณ"; + +/* No comment provided by engineer. */ +"Your settings" = "การตั้งค่าของคุณ"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "ที่อยู่ SimpleX ของคุณ"; + +/* No comment provided by engineer. */ +"Your SMP servers" = "เซิร์ฟเวอร์ SMP ของคุณ"; + +/* No comment provided by engineer. */ +"Your XFTP servers" = "เซิร์ฟเวอร์ XFTP ของคุณ"; + diff --git a/apps/ios/th.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/th.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..0ac87258df --- /dev/null +++ b/apps/ios/th.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,15 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX ต้องการการเข้าถึงกล้องเพื่อสแกนรหัสคิวอาร์เพื่อเชื่อมต่อกับผู้ใช้รายอื่นและสำหรับการโทรวิดีโอ"; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX ใช้ Face ID สำหรับการรับรองความถูกต้องในเครื่อง"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX ต้องการการเข้าถึงไมโครโฟนสำหรับการโทรด้วยเสียงและวิดีโอ และเพื่อบันทึกข้อความเสียง"; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX ต้องการเข้าถึง Photo Library เพื่อบันทึกสื่อที่ถ่ายและได้รับ"; + diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..a9213527c6 --- /dev/null +++ b/apps/ios/uk.lproj/Localizable.strings @@ -0,0 +1,3675 @@ +/* No comment provided by engineer. */ +"\n" = "\n"; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" " = " "; + +/* No comment provided by engineer. */ +" (" = " ("; + +/* No comment provided by engineer. */ +" (can be copied)" = " (можна скопіювати)"; + +/* No comment provided by engineer. */ +"_italic_" = "\\_курсив_"; + +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- стабільніша доставка повідомлень.\n- трохи кращі групи.\n- і багато іншого!"; + +/* No comment provided by engineer. */ +"- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- голосові повідомлення до 5 хвилин.\n- користувальницький час зникнення.\n- історія редагування."; + +/* No comment provided by engineer. */ +", " = ", "; + +/* No comment provided by engineer. */ +": " = ": "; + +/* No comment provided by engineer. */ +"!1 colored!" = "!1 кольоровий!"; + +/* No comment provided by engineer. */ +"." = "."; + +/* No comment provided by engineer. */ +"(" = "("; + +/* No comment provided by engineer. */ +")" = ")"; + +/* No comment provided by engineer. */ +"[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)" = "[Внесок](https://github.com/simplex-chat/simplex-chat#contribute)"; + +/* No comment provided by engineer. */ +"[Send us email](mailto:chat@simplex.chat)" = "[Напишіть нам електронною поштою](mailto:chat@simplex.chat)"; + +/* No comment provided by engineer. */ +"[Star on GitHub](https://github.com/simplex-chat/simplex-chat)" = "[Зірка на GitHub](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"**Add new contact**: to create your one-time QR Code for your contact." = "**Додати новий контакт**: щоб створити одноразовий QR-код або посилання для свого контакту."; + +/* No comment provided by engineer. */ +"**Create link / QR code** for your contact to use." = "**Створіть посилання / QR-код** для використання вашим контактом."; + +/* No comment provided by engineer. */ +"**e2e encrypted** audio call" = "**e2e encrypted** аудіодзвінок"; + +/* No comment provided by engineer. */ +"**e2e encrypted** video call" = "**e2e encrypted** відеодзвінок"; + +/* No comment provided by engineer. */ +"**More private**: check new messages every 20 minutes. Device token is shared with SimpleX Chat server, but not how many contacts or messages you have." = "**Більш приватний**: перевіряти нові повідомлення кожні 20 хвилин. Серверу SimpleX Chat передається токен пристрою, але не кількість контактів або повідомлень, які ви маєте."; + +/* No comment provided by engineer. */ +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Найбільш приватний**: не використовуйте сервер сповіщень SimpleX Chat, періодично перевіряйте повідомлення у фоновому режимі (залежить від того, як часто ви користуєтесь додатком)."; + +/* No comment provided by engineer. */ +"**Paste received link** or open it in the browser and tap **Open in mobile app**." = "**Вставте отримане посилання** або відкрийте його в браузері і натисніть **Відкрити в мобільному додатку**."; + +/* No comment provided by engineer. */ +"**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Зверніть увагу: ви НЕ зможете відновити або змінити пароль, якщо втратите його."; + +/* No comment provided by engineer. */ +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Рекомендується**: токен пристрою та сповіщення надсилаються на сервер сповіщень SimpleX Chat, але не вміст повідомлення, його розмір або від кого воно надійшло."; + +/* No comment provided by engineer. */ +"**Scan QR code**: to connect to your contact in person or via video call." = "**Відскануйте QR-код**: щоб з'єднатися з вашим контактом особисто або за допомогою відеодзвінка."; + +/* No comment provided by engineer. */ +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Попередження**: Для отримання миттєвих пуш-сповіщень потрібна парольна фраза, збережена у брелоку."; + +/* No comment provided by engineer. */ +"*bold*" = "\\*жирний*"; + +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## Історія"; + +/* copied message info */ +"## In reply to" = "## У відповідь на"; + +/* No comment provided by engineer. */ +"#secret#" = "#секрет#"; + +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"%@ (current)" = "%@ (поточний)"; + +/* copied message info */ +"%@ (current):" = "%@ (поточний):"; + +/* No comment provided by engineer. */ +"%@ / %@" = "%@ / %@"; + +/* No comment provided by engineer. */ +"%@ %@" = "%@ %@"; + +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ і %@ підключено"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "%1$@ за %2$@:"; + +/* notification title */ +"%@ is connected!" = "%@ підключено!"; + +/* No comment provided by engineer. */ +"%@ is not verified" = "%@ не перевірено"; + +/* No comment provided by engineer. */ +"%@ is verified" = "%@ перевірено"; + +/* No comment provided by engineer. */ +"%@ servers" = "%@ сервери"; + +/* notification title */ +"%@ wants to connect!" = "%@ хоче підключитися!"; + +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ та %lld інші підключені учасники"; + +/* copied message info */ +"%@:" = "%@:"; + +/* time interval */ +"%d days" = "%d днів"; + +/* time interval */ +"%d hours" = "%d годин"; + +/* time interval */ +"%d min" = "%d хв"; + +/* time interval */ +"%d months" = "%d місяців"; + +/* time interval */ +"%d sec" = "%d сек"; + +/* integrity error chat item */ +"%d skipped message(s)" = "%d пропущено повідомлення(ь)"; + +/* time interval */ +"%d weeks" = "%d тижнів"; + +/* No comment provided by engineer. */ +"%lld" = "%lld"; + +/* No comment provided by engineer. */ +"%lld %@" = "%lld %@"; + +/* No comment provided by engineer. */ +"%lld contact(s) selected" = "%lld контакт(и) вибрані"; + +/* No comment provided by engineer. */ +"%lld file(s) with total size of %@" = "%lld файл(и) загальним розміром %@"; + +/* No comment provided by engineer. */ +"%lld members" = "%lld учасників"; + +/* No comment provided by engineer. */ +"%lld minutes" = "%lld хвилин"; + +/* No comment provided by engineer. */ +"%lld second(s)" = "%lld секунд(и)"; + +/* No comment provided by engineer. */ +"%lld seconds" = "%lld секунд"; + +/* No comment provided by engineer. */ +"%lldd" = "%lldd"; + +/* No comment provided by engineer. */ +"%lldh" = "%lldh"; + +/* No comment provided by engineer. */ +"%lldk" = "%lldk"; + +/* No comment provided by engineer. */ +"%lldm" = "%lldm"; + +/* No comment provided by engineer. */ +"%lldmth" = "%lldmth"; + +/* No comment provided by engineer. */ +"%llds" = "%llds"; + +/* No comment provided by engineer. */ +"%lldw" = "%lldw"; + +/* No comment provided by engineer. */ +"%u messages failed to decrypt." = "%u повідомлень не вдалося розшифрувати."; + +/* No comment provided by engineer. */ +"%u messages skipped." = "%u повідомлень пропущено."; + +/* No comment provided by engineer. */ +"`a + b`" = "\\`a + b`"; + +/* email text */ +"<p>Hi!</p>\n<p><a href=\"%@\">Connect to me via SimpleX Chat</a></p>" = "<p>Привіт!</p>\n<p><a href=\"%@\"> Зв'яжіться зі мною через SimpleX Chat</a></p>"; + +/* No comment provided by engineer. */ +"~strike~" = "\\~закреслити~"; + +/* No comment provided by engineer. */ +"0s" = "0с"; + +/* time interval */ +"1 day" = "1 день"; + +/* time interval */ +"1 hour" = "1 година"; + +/* No comment provided by engineer. */ +"1 minute" = "1 хвилина"; + +/* time interval */ +"1 month" = "1 місяць"; + +/* time interval */ +"1 week" = "1 тиждень"; + +/* No comment provided by engineer. */ +"1-time link" = "1-разове посилання"; + +/* No comment provided by engineer. */ +"5 minutes" = "5 хвилин"; + +/* No comment provided by engineer. */ +"6" = "6"; + +/* No comment provided by engineer. */ +"30 seconds" = "30 секунд"; + +/* No comment provided by engineer. */ +"A few more things" = "Ще кілька речей"; + +/* notification title */ +"A new contact" = "Новий контакт"; + +/* No comment provided by engineer. */ +"A new random profile will be shared." = "Буде створено новий випадковий профіль."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each chat profile you have in the app**." = "Для кожного профілю чату, який ви маєте в додатку, буде використовуватися окреме TCP-з'єднання."; + +/* No comment provided by engineer. */ +"A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Для кожного контакту та учасника групи буде використовуватися окреме TCP-з'єднання.\n**Зверніть увагу: якщо у вас багато з'єднань, споживання заряду акумулятора і трафіку може бути значно вищим, а деякі з'єднання можуть обірватися."; + +/* No comment provided by engineer. */ +"Abort" = "Скасувати"; + +/* No comment provided by engineer. */ +"Abort changing address" = "Скасувати зміну адреси"; + +/* No comment provided by engineer. */ +"Abort changing address?" = "Скасувати зміну адреси?"; + +/* No comment provided by engineer. */ +"About SimpleX" = "Про SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX address" = "Про адресу SimpleX"; + +/* No comment provided by engineer. */ +"About SimpleX Chat" = "Про чат SimpleX"; + +/* No comment provided by engineer. */ +"above, then choose:" = "вище, а потім обирайте:"; + +/* No comment provided by engineer. */ +"Accent color" = "Акцентний колір"; + +/* accept contact request via notification + accept incoming call via notification */ +"Accept" = "Прийняти"; + +/* No comment provided by engineer. */ +"Accept connection request?" = "Прийняти запит на підключення?"; + +/* notification body */ +"Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; + +/* accept contact request via notification */ +"Accept incognito" = "Прийняти інкогніто"; + +/* call status */ +"accepted call" = "прийнято виклик"; + +/* 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." = "Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам."; + +/* No comment provided by engineer. */ +"Add preset servers" = "Додавання попередньо встановлених серверів"; + +/* No comment provided by engineer. */ +"Add profile" = "Додати профіль"; + +/* No comment provided by engineer. */ +"Add server…" = "Додати сервер…"; + +/* No comment provided by engineer. */ +"Add servers by scanning QR codes." = "Додайте сервери, відсканувавши QR-код."; + +/* No comment provided by engineer. */ +"Add to another device" = "Додати до іншого пристрою"; + +/* No comment provided by engineer. */ +"Add welcome message" = "Додати вітальне повідомлення"; + +/* No comment provided by engineer. */ +"Address" = "Адреса"; + +/* No comment provided by engineer. */ +"Address change will be aborted. Old receiving address will be used." = "Зміна адреси буде скасована. Буде використано стару адресу отримання."; + +/* member role */ +"admin" = "адмін"; + +/* No comment provided by engineer. */ +"Admins can create the links to join groups." = "Адміни можуть створювати посилання для приєднання до груп."; + +/* No comment provided by engineer. */ +"Advanced network settings" = "Розширені налаштування мережі"; + +/* chat item text */ +"agreeing encryption for %@…" = "узгодження шифрування для %@…"; + +/* chat item text */ +"agreeing encryption…" = "узгодження шифрування…"; + +/* No comment provided by engineer. */ +"All app data is deleted." = "Всі дані програми видаляються."; + +/* No comment provided by engineer. */ +"All chats and messages will be deleted - this cannot be undone!" = "Всі чати та повідомлення будуть видалені - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"All data is erased when it is entered." = "Всі дані стираються при введенні."; + +/* No comment provided by engineer. */ +"All group members will remain connected." = "Всі учасники групи залишаться на зв'язку."; + +/* No comment provided by engineer. */ +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Всі повідомлення будуть видалені - це неможливо скасувати! Повідомлення будуть видалені ТІЛЬКИ для вас."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected." = "Всі ваші контакти залишаться на зв'язку."; + +/* No comment provided by engineer. */ +"All your contacts will remain connected. Profile update will be sent to your contacts." = "Всі ваші контакти залишаться на зв'язку. Повідомлення про оновлення профілю буде надіслано вашим контактам."; + +/* No comment provided by engineer. */ +"Allow" = "Дозволити"; + +/* No comment provided by engineer. */ +"Allow calls only if your contact allows them." = "Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх."; + +/* No comment provided by engineer. */ +"Allow disappearing messages only if your contact allows it to you." = "Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити."; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you." = "Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити."; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх."; + +/* No comment provided by engineer. */ +"Allow message reactions." = "Дозволити реакцію на повідомлення."; + +/* No comment provided by engineer. */ +"Allow sending direct messages to members." = "Дозволяє надсилати прямі повідомлення користувачам."; + +/* No comment provided by engineer. */ +"Allow sending disappearing messages." = "Дозволити надсилання зникаючих повідомлень."; + +/* No comment provided by engineer. */ +"Allow to irreversibly delete sent messages." = "Дозволяє безповоротно видаляти надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Allow to send files and media." = "Дозволяє надсилати файли та медіа."; + +/* No comment provided by engineer. */ +"Allow to send voice messages." = "Дозволити надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Allow voice messages only if your contact allows them." = "Дозволяйте голосові повідомлення, тільки якщо ваш контакт дозволяє їх."; + +/* No comment provided by engineer. */ +"Allow voice messages?" = "Дозволити голосові повідомлення?"; + +/* No comment provided by engineer. */ +"Allow your contacts adding message reactions." = "Дозвольте вашим контактам додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Allow your contacts to call you." = "Дозвольте вашим контактам телефонувати вам."; + +/* No comment provided by engineer. */ +"Allow your contacts to irreversibly delete sent messages." = "Дозвольте вашим контактам безповоротно видаляти надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Allow your contacts to send disappearing messages." = "Дозвольте своїм контактам надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Allow your contacts to send voice messages." = "Дозвольте своїм контактам надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Already connected?" = "Вже підключено?"; + +/* pref value */ +"always" = "завжди"; + +/* No comment provided by engineer. */ +"Always use relay" = "Завжди використовуйте реле"; + +/* No comment provided by engineer. */ +"An empty chat profile with the provided name is created, and the app opens as usual." = "Створюється порожній профіль чату з вказаним ім'ям, і додаток відкривається у звичайному режимі."; + +/* No comment provided by engineer. */ +"Answer call" = "Відповісти на дзвінок"; + +/* No comment provided by engineer. */ +"App build: %@" = "Збірка програми: %@"; + +/* No comment provided by engineer. */ +"App icon" = "Іконка програми"; + +/* No comment provided by engineer. */ +"App passcode" = "Пароль додатку"; + +/* No comment provided by engineer. */ +"App passcode is replaced with self-destruct passcode." = "Пароль програми замінено на пароль самознищення."; + +/* No comment provided by engineer. */ +"App version" = "Версія програми"; + +/* No comment provided by engineer. */ +"App version: v%@" = "Версія програми: v%@"; + +/* No comment provided by engineer. */ +"Appearance" = "Зовнішній вигляд"; + +/* No comment provided by engineer. */ +"Attach" = "Прикріпити"; + +/* No comment provided by engineer. */ +"Audio & video calls" = "Аудіо та відео дзвінки"; + +/* No comment provided by engineer. */ +"Audio and video calls" = "Аудіо та відеодзвінки"; + +/* No comment provided by engineer. */ +"audio call (not e2e encrypted)" = "аудіовиклик (без шифрування e2e)"; + +/* chat feature */ +"Audio/video calls" = "Аудіо/відео дзвінки"; + +/* No comment provided by engineer. */ +"Audio/video calls are prohibited." = "Аудіо/відео дзвінки заборонені."; + +/* PIN entry */ +"Authentication cancelled" = "Аутентифікацію скасовано"; + +/* No comment provided by engineer. */ +"Authentication failed" = "Не вдалося пройти автентифікацію"; + +/* No comment provided by engineer. */ +"Authentication is required before the call is connected, but you may miss calls." = "Перед з'єднанням дзвінка потрібно пройти автентифікацію, але ви можете пропустити дзвінки."; + +/* No comment provided by engineer. */ +"Authentication unavailable" = "Автентифікація недоступна"; + +/* No comment provided by engineer. */ +"Auto-accept" = "Автоприйняття"; + +/* No comment provided by engineer. */ +"Auto-accept contact requests" = "Автоматичне прийняття запитів на контакт"; + +/* No comment provided by engineer. */ +"Auto-accept images" = "Автоматичне прийняття зображень"; + +/* No comment provided by engineer. */ +"Back" = "Назад"; + +/* integrity error chat item */ +"bad message hash" = "невірний хеш повідомлення"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Поганий хеш повідомлення"; + +/* integrity error chat item */ +"bad message ID" = "невірний ідентифікатор повідомлення"; + +/* No comment provided by engineer. */ +"Bad message ID" = "Неправильний ідентифікатор повідомлення"; + +/* No comment provided by engineer. */ +"Better messages" = "Кращі повідомлення"; + +/* No comment provided by engineer. */ +"bold" = "жирний"; + +/* No comment provided by engineer. */ +"Both you and your contact can add message reactions." = "Реакції на повідомлення можете додавати як ви, так і ваш контакт."; + +/* No comment provided by engineer. */ +"Both you and your contact can irreversibly delete sent messages." = "І ви, і ваш контакт можете безповоротно видалити надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Both you and your contact can make calls." = "Дзвонити можете як ви, так і ваш контакт."; + +/* No comment provided by engineer. */ +"Both you and your contact can send disappearing messages." = "Ви і ваш контакт можете надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Both you and your contact can send voice messages." = "Надсилати голосові повідомлення можете як ви, так і ваш контакт."; + +/* No comment provided by engineer. */ +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; + +/* No comment provided by engineer. */ +"Call already ended!" = "Дзвінок вже закінчився!"; + +/* call status */ +"call error" = "помилка дзвінка"; + +/* call status */ +"call in progress" = "виклик у процесі"; + +/* call status */ +"calling…" = "дзвоніть…"; + +/* No comment provided by engineer. */ +"Calls" = "Дзвінки"; + +/* No comment provided by engineer. */ +"Can't delete user profile!" = "Не можу видалити профіль користувача!"; + +/* No comment provided by engineer. */ +"Can't invite contact!" = "Не вдається запросити контакт!"; + +/* No comment provided by engineer. */ +"Can't invite contacts!" = "Неможливо запросити контакти!"; + +/* No comment provided by engineer. */ +"Cancel" = "Скасувати"; + +/* feature offered item */ +"cancelled %@" = "скасовано %@"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних"; + +/* No comment provided by engineer. */ +"Cannot receive file" = "Не вдається отримати файл"; + +/* No comment provided by engineer. */ +"Change" = "Зміна"; + +/* No comment provided by engineer. */ +"Change database passphrase?" = "Змінити пароль до бази даних?"; + +/* authentication reason */ +"Change lock mode" = "Зміна режиму блокування"; + +/* No comment provided by engineer. */ +"Change member role?" = "Змінити роль учасника?"; + +/* authentication reason */ +"Change passcode" = "Змінити пароль"; + +/* No comment provided by engineer. */ +"Change receiving address" = "Змінити адресу отримання"; + +/* No comment provided by engineer. */ +"Change receiving address?" = "Змінити адресу отримання?"; + +/* No comment provided by engineer. */ +"Change role" = "Змінити роль"; + +/* authentication reason */ +"Change self-destruct mode" = "Змінити режим самознищення"; + +/* authentication reason + set passcode view */ +"Change self-destruct passcode" = "Змінити пароль самознищення"; + +/* chat item text */ +"changed address for you" = "змінили для вас адресу"; + +/* rcv group event chat item */ +"changed role of %@ to %@" = "змінено роль %1$@ на %2$@"; + +/* rcv group event chat item */ +"changed your role to %@" = "змінили свою роль на %@"; + +/* chat item text */ +"changing address for %@…" = "зміна адреси для %@…"; + +/* chat item text */ +"changing address…" = "змінює адресу…"; + +/* No comment provided by engineer. */ +"Chat archive" = "Архів чату"; + +/* No comment provided by engineer. */ +"Chat console" = "Консоль чату"; + +/* No comment provided by engineer. */ +"Chat database" = "База даних чату"; + +/* No comment provided by engineer. */ +"Chat database deleted" = "Видалено базу даних чату"; + +/* No comment provided by engineer. */ +"Chat database imported" = "Імпорт бази даних чату"; + +/* No comment provided by engineer. */ +"Chat is running" = "Чат запущено"; + +/* No comment provided by engineer. */ +"Chat is stopped" = "Чат зупинено"; + +/* No comment provided by engineer. */ +"Chat preferences" = "Налаштування чату"; + +/* No comment provided by engineer. */ +"Chats" = "Чати"; + +/* No comment provided by engineer. */ +"Check server address and try again." = "Перевірте адресу сервера та спробуйте ще раз."; + +/* No comment provided by engineer. */ +"Chinese and Spanish interface" = "Інтерфейс китайською та іспанською мовами"; + +/* No comment provided by engineer. */ +"Choose file" = "Виберіть файл"; + +/* No comment provided by engineer. */ +"Choose from library" = "Виберіть з бібліотеки"; + +/* No comment provided by engineer. */ +"Clear" = "Чисто"; + +/* No comment provided by engineer. */ +"Clear conversation" = "Ясна розмова"; + +/* No comment provided by engineer. */ +"Clear conversation?" = "Відверта розмова?"; + +/* No comment provided by engineer. */ +"Clear verification" = "Очистити перевірку"; + +/* No comment provided by engineer. */ +"colored" = "кольоровий"; + +/* No comment provided by engineer. */ +"Colors" = "Кольори"; + +/* server test step */ +"Compare file" = "Порівняти файл"; + +/* No comment provided by engineer. */ +"Compare security codes with your contacts." = "Порівняйте коди безпеки зі своїми контактами."; + +/* No comment provided by engineer. */ +"complete" = "завершено"; + +/* No comment provided by engineer. */ +"Configure ICE servers" = "Налаштування серверів ICE"; + +/* No comment provided by engineer. */ +"Confirm" = "Підтвердити"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Підтвердити оновлення бази даних"; + +/* No comment provided by engineer. */ +"Confirm new passphrase…" = "Підтвердіть нову парольну фразу…"; + +/* No comment provided by engineer. */ +"Confirm Passcode" = "Підтвердити пароль"; + +/* No comment provided by engineer. */ +"Confirm password" = "Підтвердити пароль"; + +/* server test step */ +"Connect" = "Підключіться"; + +/* No comment provided by engineer. */ +"Connect directly" = "Підключіться безпосередньо"; + +/* No comment provided by engineer. */ +"Connect incognito" = "Підключайтеся інкогніто"; + +/* No comment provided by engineer. */ +"connect to SimpleX Chat developers." = "зв'язатися з розробниками SimpleX Chat."; + +/* No comment provided by engineer. */ +"Connect via contact link" = "Підключіться за контактним посиланням"; + +/* No comment provided by engineer. */ +"Connect via group link?" = "Підключитися за груповим посиланням?"; + +/* No comment provided by engineer. */ +"Connect via link" = "Підключіться за посиланням"; + +/* No comment provided by engineer. */ +"Connect via link / QR code" = "Підключитися за посиланням / QR-кодом"; + +/* No comment provided by engineer. */ +"Connect via one-time link" = "Під'єднатися за одноразовим посиланням"; + +/* No comment provided by engineer. */ +"connected" = "з'єднаний"; + +/* No comment provided by engineer. */ +"connecting" = "з'єднання"; + +/* No comment provided by engineer. */ +"connecting (accepted)" = "з'єднання (прийнято)"; + +/* No comment provided by engineer. */ +"connecting (announced)" = "з'єднання (оголошено)"; + +/* No comment provided by engineer. */ +"connecting (introduced)" = "з'єднання (введено)"; + +/* No comment provided by engineer. */ +"connecting (introduction invitation)" = "з'єднання (вступне запрошення)"; + +/* call status */ +"connecting call" = "підключення дзвінка…"; + +/* No comment provided by engineer. */ +"Connecting server…" = "Підключення до сервера…"; + +/* No comment provided by engineer. */ +"Connecting server… (error: %@)" = "Підключення до сервера... (помилка: %@)"; + +/* chat list item title */ +"connecting…" = "з'єднання…"; + +/* No comment provided by engineer. */ +"Connection" = "Підключення"; + +/* No comment provided by engineer. */ +"Connection error" = "Помилка підключення"; + +/* No comment provided by engineer. */ +"Connection error (AUTH)" = "Помилка підключення (AUTH)"; + +/* chat list item title (it should not be shown */ +"connection established" = "з'єднання встановлене"; + +/* No comment provided by engineer. */ +"Connection request sent!" = "Запит на підключення відправлено!"; + +/* No comment provided by engineer. */ +"Connection timeout" = "Тайм-аут з'єднання"; + +/* connection information */ +"connection:%@" = "з'єднання:%@"; + +/* No comment provided by engineer. */ +"Contact allows" = "Контакт дозволяє"; + +/* No comment provided by engineer. */ +"Contact already exists" = "Контакт вже існує"; + +/* No comment provided by engineer. */ +"Contact and all messages will be deleted - this cannot be undone!" = "Контакт і всі повідомлення будуть видалені - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"contact has e2e encryption" = "контакт має шифрування e2e"; + +/* No comment provided by engineer. */ +"contact has no e2e encryption" = "контакт не має шифрування e2e"; + +/* notification */ +"Contact hidden:" = "Контакт приховано:"; + +/* notification */ +"Contact is connected" = "Контакт підключений"; + +/* No comment provided by engineer. */ +"Contact is not connected yet!" = "Контакт ще не підключено!"; + +/* No comment provided by engineer. */ +"Contact name" = "Ім'я контактної особи"; + +/* No comment provided by engineer. */ +"Contact preferences" = "Налаштування контактів"; + +/* No comment provided by engineer. */ +"Contacts" = "Контакти"; + +/* No comment provided by engineer. */ +"Contacts can mark messages for deletion; you will be able to view them." = "Контакти можуть позначати повідомлення для видалення; ви зможете їх переглянути."; + +/* No comment provided by engineer. */ +"Continue" = "Продовжуйте"; + +/* chat item action */ +"Copy" = "Копіювати"; + +/* No comment provided by engineer. */ +"Core version: v%@" = "Основна версія: v%@"; + +/* No comment provided by engineer. */ +"Create" = "Створити"; + +/* No comment provided by engineer. */ +"Create an address to let people connect with you." = "Створіть адресу, щоб люди могли з вами зв'язатися."; + +/* server test step */ +"Create file" = "Створити файл"; + +/* No comment provided by engineer. */ +"Create group link" = "Створити групове посилання"; + +/* No comment provided by engineer. */ +"Create link" = "Створити посилання"; + +/* No comment provided by engineer. */ +"Create one-time invitation link" = "Створіть одноразове посилання-запрошення"; + +/* server test step */ +"Create queue" = "Створити чергу"; + +/* No comment provided by engineer. */ +"Create secret group" = "Створити секретну групу"; + +/* No comment provided by engineer. */ +"Create SimpleX address" = "Створіть адресу SimpleX"; + +/* No comment provided by engineer. */ +"Create your profile" = "Створіть свій профіль"; + +/* No comment provided by engineer. */ +"Created on %@" = "Створено %@"; + +/* No comment provided by engineer. */ +"creator" = "творець"; + +/* No comment provided by engineer. */ +"Current Passcode" = "Поточний пароль"; + +/* No comment provided by engineer. */ +"Current passphrase…" = "Поточна парольна фраза…"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; + +/* dropdown time picker choice */ +"custom" = "звичайний"; + +/* No comment provided by engineer. */ +"Custom time" = "Індивідуальний час"; + +/* No comment provided by engineer. */ +"Dark" = "Темний"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Пониження версії бази даних"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "База даних зашифрована!"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Парольна фраза шифрування бази даних буде оновлена та збережена у в’язці ключів.\n"; + +/* No comment provided by engineer. */ +"Database encryption passphrase will be updated.\n" = "Ключову фразу шифрування бази даних буде оновлено.\n"; + +/* No comment provided by engineer. */ +"Database error" = "Помилка в базі даних"; + +/* No comment provided by engineer. */ +"Database ID" = "Ідентифікатор бази даних"; + +/* copied message info */ +"Database ID: %d" = "Ідентифікатор бази даних: %d"; + +/* No comment provided by engineer. */ +"Database IDs and Transport isolation option." = "Ідентифікатори бази даних та опція ізоляції транспорту."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase, you can change it." = "База даних зашифрована за допомогою випадкової парольної фрази, яку ви можете змінити."; + +/* No comment provided by engineer. */ +"Database is encrypted using a random passphrase. Please change it before exporting." = "База даних зашифрована за допомогою випадкової парольної фрази. Будь ласка, змініть його перед експортом."; + +/* No comment provided by engineer. */ +"Database passphrase" = "Ключова фраза бази даних"; + +/* No comment provided by engineer. */ +"Database passphrase & export" = "Ключова фраза бази даних та експорт"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Парольна фраза бази даних відрізняється від збереженої у в’язці ключів."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Для відкриття чату потрібно ввести пароль до бази даних."; + +/* No comment provided by engineer. */ +"Database upgrade" = "Оновлення бази даних"; + +/* No comment provided by engineer. */ +"database version is newer than the app, but no down migration for: %@" = "версія бази даних новіша, ніж додаток, але без міграції вниз для: %@"; + +/* No comment provided by engineer. */ +"Database will be encrypted and the passphrase stored in the keychain.\n" = "База даних буде зашифрована, а парольна фраза збережена у в’язці ключів.\n"; + +/* No comment provided by engineer. */ +"Database will be encrypted.\n" = "База даних буде зашифрована.\n"; + +/* No comment provided by engineer. */ +"Database will be migrated when the app restarts" = "База даних буде перенесена під час перезапуску програми"; + +/* time unit */ +"days" = "днів"; + +/* No comment provided by engineer. */ +"Decentralized" = "Децентралізований"; + +/* message decrypt error item */ +"Decryption error" = "Помилка розшифровки"; + +/* pref value */ +"default (%@)" = "за замовчуванням (%@)"; + +/* No comment provided by engineer. */ +"default (no)" = "за замовчуванням (ні)"; + +/* No comment provided by engineer. */ +"default (yes)" = "за замовчуванням (так)"; + +/* chat item action */ +"Delete" = "Видалити"; + +/* No comment provided by engineer. */ +"Delete address" = "Видалити адресу"; + +/* No comment provided by engineer. */ +"Delete address?" = "Видалити адресу?"; + +/* No comment provided by engineer. */ +"Delete after" = "Видалити після"; + +/* No comment provided by engineer. */ +"Delete all files" = "Видалити всі файли"; + +/* No comment provided by engineer. */ +"Delete archive" = "Видалити архів"; + +/* No comment provided by engineer. */ +"Delete chat archive?" = "Видалити архів чату?"; + +/* No comment provided by engineer. */ +"Delete chat profile" = "Видалити профіль чату"; + +/* No comment provided by engineer. */ +"Delete chat profile?" = "Видалити профіль чату?"; + +/* No comment provided by engineer. */ +"Delete connection" = "Видалити підключення"; + +/* No comment provided by engineer. */ +"Delete contact" = "Видалити контакт"; + +/* No comment provided by engineer. */ +"Delete Contact" = "Видалити контакт"; + +/* No comment provided by engineer. */ +"Delete contact?" = "Видалити контакт?"; + +/* No comment provided by engineer. */ +"Delete database" = "Видалити базу даних"; + +/* server test step */ +"Delete file" = "Видалити файл"; + +/* No comment provided by engineer. */ +"Delete files and media?" = "Видаляти файли та медіа?"; + +/* No comment provided by engineer. */ +"Delete files for all chat profiles" = "Видалення файлів для всіх профілів чату"; + +/* chat feature */ +"Delete for everyone" = "Видалити для всіх"; + +/* No comment provided by engineer. */ +"Delete for me" = "Видалити для мене"; + +/* No comment provided by engineer. */ +"Delete group" = "Видалити групу"; + +/* No comment provided by engineer. */ +"Delete group?" = "Видалити групу?"; + +/* No comment provided by engineer. */ +"Delete invitation" = "Видалити запрошення"; + +/* No comment provided by engineer. */ +"Delete link" = "Видалити посилання"; + +/* No comment provided by engineer. */ +"Delete link?" = "Видалити посилання?"; + +/* No comment provided by engineer. */ +"Delete member message?" = "Видалити повідомлення учасника?"; + +/* No comment provided by engineer. */ +"Delete message?" = "Видалити повідомлення?"; + +/* No comment provided by engineer. */ +"Delete messages" = "Видалити повідомлення"; + +/* No comment provided by engineer. */ +"Delete messages after" = "Видаляйте повідомлення після"; + +/* No comment provided by engineer. */ +"Delete old database" = "Видалення старої бази даних"; + +/* No comment provided by engineer. */ +"Delete old database?" = "Видалити стару базу даних?"; + +/* No comment provided by engineer. */ +"Delete pending connection" = "Видалити очікуване з'єднання"; + +/* No comment provided by engineer. */ +"Delete pending connection?" = "Видалити очікуване з'єднання?"; + +/* No comment provided by engineer. */ +"Delete profile" = "Видалити профіль"; + +/* server test step */ +"Delete queue" = "Видалити чергу"; + +/* No comment provided by engineer. */ +"Delete user profile?" = "Видалити профіль користувача?"; + +/* deleted chat item */ +"deleted" = "видалено"; + +/* No comment provided by engineer. */ +"Deleted at" = "Видалено за"; + +/* copied message info */ +"Deleted at: %@" = "Видалено за: %@"; + +/* rcv group event chat item */ +"deleted group" = "видалено групу"; + +/* No comment provided by engineer. */ +"Delivery" = "Доставка"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "Квитанції про доставку відключені!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "Квитанції про доставку!"; + +/* No comment provided by engineer. */ +"Description" = "Опис"; + +/* No comment provided by engineer. */ +"Develop" = "Розробник"; + +/* No comment provided by engineer. */ +"Developer tools" = "Інструменти для розробників"; + +/* No comment provided by engineer. */ +"Device" = "Пристрій"; + +/* No comment provided by engineer. */ +"Device authentication is disabled. Turning off SimpleX Lock." = "Автентифікацію пристрою вимкнено. Вимкнення SimpleX Lock."; + +/* No comment provided by engineer. */ +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Автентифікація пристрою не ввімкнена. Ви можете увімкнути SimpleX Lock у Налаштуваннях, коли увімкнете автентифікацію пристрою."; + +/* No comment provided by engineer. */ +"different migration in the app/database: %@ / %@" = "різна міграція в додатку/базі даних: %@ / %@"; + +/* No comment provided by engineer. */ +"Different names, avatars and transport isolation." = "Різні імена, аватарки та транспортна ізоляція."; + +/* connection level description */ +"direct" = "прямо"; + +/* chat feature */ +"Direct messages" = "Прямі повідомлення"; + +/* No comment provided by engineer. */ +"Direct messages between members are prohibited in this group." = "У цій групі заборонені прямі повідомлення між учасниками."; + +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "Вимкнути (зберегти перевизначення)"; + +/* No comment provided by engineer. */ +"Disable for all" = "Вимкнути для всіх"; + +/* authentication reason */ +"Disable SimpleX Lock" = "Вимкнути SimpleX Lock"; + +/* No comment provided by engineer. */ +"disabled" = "вимкнено"; + +/* No comment provided by engineer. */ +"Disappearing message" = "Зникаюче повідомлення"; + +/* chat feature */ +"Disappearing messages" = "Зникаючі повідомлення"; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this chat." = "Зникаючі повідомлення в цьому чаті заборонені."; + +/* No comment provided by engineer. */ +"Disappearing messages are prohibited in this group." = "У цій групі заборонено зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Disappears at" = "Зникає за"; + +/* copied message info */ +"Disappears at: %@" = "Зникає за: %@"; + +/* server test step */ +"Disconnect" = "Від'єднати"; + +/* No comment provided by engineer. */ +"Display name" = "Відображуване ім'я"; + +/* No comment provided by engineer. */ +"Display name:" = "Відображуване ім'я:"; + +/* No comment provided by engineer. */ +"Do it later" = "Зробіть це пізніше"; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "НЕ використовуйте SimpleX для екстрених викликів."; + +/* No comment provided by engineer. */ +"Don't create address" = "Не створювати адресу"; + +/* No comment provided by engineer. */ +"Don't enable" = "Не вмикати"; + +/* No comment provided by engineer. */ +"Don't show again" = "Більше не показувати"; + +/* No comment provided by engineer. */ +"Downgrade and open chat" = "Пониження та відкритий чат"; + +/* server test step */ +"Download file" = "Завантажити файл"; + +/* No comment provided by engineer. */ +"Duplicate display name!" = "Дублююче ім'я користувача!"; + +/* integrity error chat item */ +"duplicate message" = "дублююче повідомлення"; + +/* No comment provided by engineer. */ +"Duration" = "Тривалість"; + +/* No comment provided by engineer. */ +"e2e encrypted" = "e2e зашифрований"; + +/* chat item action */ +"Edit" = "Редагувати"; + +/* No comment provided by engineer. */ +"Edit group profile" = "Редагування профілю групи"; + +/* No comment provided by engineer. */ +"Enable" = "Увімкнути"; + +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "Увімкнути (зберегти перевизначення)"; + +/* No comment provided by engineer. */ +"Enable automatic message deletion?" = "Увімкнути автоматичне видалення повідомлень?"; + +/* No comment provided by engineer. */ +"Enable for all" = "Увімкнути для всіх"; + +/* No comment provided by engineer. */ +"Enable instant notifications?" = "Увімкнути миттєві сповіщення?"; + +/* No comment provided by engineer. */ +"Enable lock" = "Увімкнути блокування"; + +/* No comment provided by engineer. */ +"Enable notifications" = "Увімкнути сповіщення"; + +/* No comment provided by engineer. */ +"Enable periodic notifications?" = "Увімкнути періодичні сповіщення?"; + +/* No comment provided by engineer. */ +"Enable self-destruct" = "Увімкнути самознищення"; + +/* set passcode view */ +"Enable self-destruct passcode" = "Увімкнути пароль самознищення"; + +/* authentication reason */ +"Enable SimpleX Lock" = "Увімкнути SimpleX Lock"; + +/* No comment provided by engineer. */ +"Enable TCP keep-alive" = "Увімкнути TCP keep-alive"; + +/* enabled status */ +"enabled" = "увімкнено"; + +/* enabled status */ +"enabled for contact" = "увімкнено для контакту"; + +/* enabled status */ +"enabled for you" = "увімкнено для вас"; + +/* No comment provided by engineer. */ +"Encrypt" = "Зашифрувати"; + +/* No comment provided by engineer. */ +"Encrypt database?" = "Зашифрувати базу даних?"; + +/* No comment provided by engineer. */ +"Encrypted database" = "Зашифрована база даних"; + +/* notification */ +"Encrypted message or another event" = "Зашифроване повідомлення або інша подія"; + +/* notification */ +"Encrypted message: database error" = "Зашифроване повідомлення: помилка бази даних"; + +/* notification */ +"Encrypted message: database migration error" = "Зашифроване повідомлення: помилка міграції бази даних"; + +/* notification */ +"Encrypted message: keychain error" = "Зашифроване повідомлення: помилка ланцюжка ключів"; + +/* notification */ +"Encrypted message: no passphrase" = "Зашифроване повідомлення: без ключової фрази"; + +/* notification */ +"Encrypted message: unexpected error" = "Зашифроване повідомлення: несподівана помилка"; + +/* chat item text */ +"encryption agreed" = "узгоджено шифрування"; + +/* chat item text */ +"encryption agreed for %@" = "узгоджене шифрування для %@"; + +/* chat item text */ +"encryption ok" = "шифрування ok"; + +/* chat item text */ +"encryption ok for %@" = "шифрування ok для %@"; + +/* chat item text */ +"encryption re-negotiation allowed" = "переузгодження шифрування дозволено"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "переузгодження шифрування дозволено для %@"; + +/* chat item text */ +"encryption re-negotiation required" = "потрібне повторне узгодження шифрування"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "для %@ потрібне повторне узгодження шифрування"; + +/* No comment provided by engineer. */ +"ended" = "закінчився"; + +/* call status */ +"ended call %@" = "закінчився виклик %@"; + +/* No comment provided by engineer. */ +"Enter correct passphrase." = "Введіть правильну парольну фразу."; + +/* No comment provided by engineer. */ +"Enter Passcode" = "Введіть пароль"; + +/* No comment provided by engineer. */ +"Enter passphrase…" = "Введіть пароль…"; + +/* No comment provided by engineer. */ +"Enter password above to show!" = "Введіть пароль вище, щоб показати!"; + +/* No comment provided by engineer. */ +"Enter server manually" = "Увійдіть на сервер вручну"; + +/* placeholder */ +"Enter welcome message…" = "Введіть вітальне повідомлення…"; + +/* placeholder */ +"Enter welcome message… (optional)" = "Введіть вітальне повідомлення... (необов'язково)"; + +/* No comment provided by engineer. */ +"error" = "помилка"; + +/* No comment provided by engineer. */ +"Error" = "Помилка"; + +/* No comment provided by engineer. */ +"Error aborting address change" = "Помилка скасування зміни адреси"; + +/* No comment provided by engineer. */ +"Error accepting contact request" = "Помилка при прийнятті запиту на контакт"; + +/* No comment provided by engineer. */ +"Error accessing database file" = "Помилка доступу до файлу бази даних"; + +/* No comment provided by engineer. */ +"Error adding member(s)" = "Помилка додавання користувача(ів)"; + +/* No comment provided by engineer. */ +"Error changing address" = "Помилка зміни адреси"; + +/* No comment provided by engineer. */ +"Error changing role" = "Помилка зміни ролі"; + +/* No comment provided by engineer. */ +"Error changing setting" = "Помилка зміни налаштування"; + +/* No comment provided by engineer. */ +"Error creating address" = "Помилка створення адреси"; + +/* No comment provided by engineer. */ +"Error creating group" = "Помилка створення групи"; + +/* No comment provided by engineer. */ +"Error creating group link" = "Помилка створення посилання на групу"; + +/* No comment provided by engineer. */ +"Error creating profile!" = "Помилка створення профілю!"; + +/* No comment provided by engineer. */ +"Error deleting chat database" = "Помилка видалення бази даних чату"; + +/* No comment provided by engineer. */ +"Error deleting chat!" = "Помилка видалення чату!"; + +/* No comment provided by engineer. */ +"Error deleting connection" = "Помилка видалення з'єднання"; + +/* No comment provided by engineer. */ +"Error deleting contact" = "Помилка видалення контакту"; + +/* No comment provided by engineer. */ +"Error deleting database" = "Помилка видалення бази даних"; + +/* No comment provided by engineer. */ +"Error deleting old database" = "Помилка видалення старої бази даних"; + +/* No comment provided by engineer. */ +"Error deleting token" = "Помилка видалення токена"; + +/* No comment provided by engineer. */ +"Error deleting user profile" = "Помилка видалення профілю користувача"; + +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "Помилка активації підтвердження доставлення!"; + +/* No comment provided by engineer. */ +"Error enabling notifications" = "Помилка увімкнення сповіщень"; + +/* No comment provided by engineer. */ +"Error encrypting database" = "Помилка шифрування бази даних"; + +/* No comment provided by engineer. */ +"Error exporting chat database" = "Помилка експорту бази даних чату"; + +/* No comment provided by engineer. */ +"Error importing chat database" = "Помилка імпорту бази даних чату"; + +/* No comment provided by engineer. */ +"Error joining group" = "Помилка приєднання до групи"; + +/* No comment provided by engineer. */ +"Error loading %@ servers" = "Помилка завантаження %@ серверів"; + +/* No comment provided by engineer. */ +"Error receiving file" = "Помилка отримання файлу"; + +/* No comment provided by engineer. */ +"Error removing member" = "Помилка видалення учасника"; + +/* No comment provided by engineer. */ +"Error saving %@ servers" = "Помилка збереження %@ серверів"; + +/* No comment provided by engineer. */ +"Error saving group profile" = "Помилка збереження профілю групи"; + +/* No comment provided by engineer. */ +"Error saving ICE servers" = "Помилка збереження серверів ICE"; + +/* No comment provided by engineer. */ +"Error saving passcode" = "Помилка збереження пароля"; + +/* No comment provided by engineer. */ +"Error saving passphrase to keychain" = "Помилка збереження пароля на keychain"; + +/* No comment provided by engineer. */ +"Error saving user password" = "Помилка збереження пароля користувача"; + +/* No comment provided by engineer. */ +"Error sending email" = "Помилка надсилання електронного листа"; + +/* No comment provided by engineer. */ +"Error sending message" = "Помилка надсилання повідомлення"; + +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "Помилка встановлення підтвердження доставлення!"; + +/* No comment provided by engineer. */ +"Error starting chat" = "Помилка запуску чату"; + +/* No comment provided by engineer. */ +"Error stopping chat" = "Помилка зупинки чату"; + +/* No comment provided by engineer. */ +"Error switching profile!" = "Помилка перемикання профілю!"; + +/* No comment provided by engineer. */ +"Error synchronizing connection" = "Помилка синхронізації з'єднання"; + +/* No comment provided by engineer. */ +"Error updating group link" = "Помилка оновлення посилання на групу"; + +/* No comment provided by engineer. */ +"Error updating message" = "Повідомлення про помилку оновлення"; + +/* No comment provided by engineer. */ +"Error updating settings" = "Помилка оновлення налаштувань"; + +/* No comment provided by engineer. */ +"Error updating user privacy" = "Помилка оновлення конфіденційності користувача"; + +/* No comment provided by engineer. */ +"Error: " = "Помилка: "; + +/* No comment provided by engineer. */ +"Error: %@" = "Помилка: %@"; + +/* No comment provided by engineer. */ +"Error: no database file" = "Помилка: немає файлу бази даних"; + +/* No comment provided by engineer. */ +"Error: URL is invalid" = "Помилка: URL-адреса невірна"; + +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "Навіть коли вимкнений у розмові."; + +/* No comment provided by engineer. */ +"event happened" = "відбулася подія"; + +/* No comment provided by engineer. */ +"Exit without saving" = "Вихід без збереження"; + +/* No comment provided by engineer. */ +"Export database" = "Експорт бази даних"; + +/* No comment provided by engineer. */ +"Export error:" = "Помилка експорту:"; + +/* No comment provided by engineer. */ +"Exported database archive." = "Експортований архів бази даних."; + +/* No comment provided by engineer. */ +"Exporting database archive…" = "Експорт архіву бази даних…"; + +/* No comment provided by engineer. */ +"Failed to remove passphrase" = "Не вдалося видалити парольну фразу"; + +/* No comment provided by engineer. */ +"Fast and no wait until the sender is online!" = "Швидко і без очікування, поки відправник буде онлайн!"; + +/* No comment provided by engineer. */ +"Favorite" = "Улюблений"; + +/* No comment provided by engineer. */ +"File will be deleted from servers." = "Файл буде видалено з серверів."; + +/* No comment provided by engineer. */ +"File will be received when your contact completes uploading it." = "Файл буде отримано, коли ваш контакт завершить завантаження."; + +/* No comment provided by engineer. */ +"File will be received when your contact is online, please wait or check later!" = "Файл буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"File: %@" = "Файл: %@"; + +/* No comment provided by engineer. */ +"Files & media" = "Файли та медіа"; + +/* chat feature */ +"Files and media" = "Файли і медіа"; + +/* No comment provided by engineer. */ +"Files and media are prohibited in this group." = "Файли та медіа в цій групі заборонені."; + +/* No comment provided by engineer. */ +"Files and media prohibited!" = "Файли та медіа заборонені!"; + +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "Фільтруйте непрочитані та улюблені чати."; + +/* No comment provided by engineer. */ +"Finally, we have them! 🚀" = "Нарешті, вони у нас є! 🚀"; + +/* No comment provided by engineer. */ +"Find chats faster" = "Швидше знаходьте чати"; + +/* No comment provided by engineer. */ +"Fix" = "Виправити"; + +/* No comment provided by engineer. */ +"Fix connection" = "Виправити з'єднання"; + +/* No comment provided by engineer. */ +"Fix connection?" = "Полагодити зв'язок?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "Виправити шифрування після відновлення резервних копій."; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "Виправлення не підтримується контактом"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "Виправлення не підтримується учасником групи"; + +/* No comment provided by engineer. */ +"For console" = "Для консолі"; + +/* No comment provided by engineer. */ +"French interface" = "Французький інтерфейс"; + +/* No comment provided by engineer. */ +"Full link" = "Повне посилання"; + +/* No comment provided by engineer. */ +"Full name (optional)" = "Повне ім'я (необов'язково)"; + +/* No comment provided by engineer. */ +"Full name:" = "Повне ім'я:"; + +/* No comment provided by engineer. */ +"Fully re-implemented - work in background!" = "Повністю перероблено - робота у фоновому режимі!"; + +/* No comment provided by engineer. */ +"Further reduced battery usage" = "Подальше зменшення використання акумулятора"; + +/* No comment provided by engineer. */ +"GIFs and stickers" = "GIF-файли та наклейки"; + +/* No comment provided by engineer. */ +"Group" = "Група"; + +/* No comment provided by engineer. */ +"group deleted" = "групу видалено"; + +/* No comment provided by engineer. */ +"Group display name" = "Назва групи для відображення"; + +/* No comment provided by engineer. */ +"Group full name (optional)" = "Повна назва групи (необов'язково)"; + +/* No comment provided by engineer. */ +"Group image" = "Зображення групи"; + +/* No comment provided by engineer. */ +"Group invitation" = "Групове запрошення"; + +/* No comment provided by engineer. */ +"Group invitation expired" = "Термін дії групового запрошення закінчився"; + +/* No comment provided by engineer. */ +"Group invitation is no longer valid, it was removed by sender." = "Групове запрошення більше не дійсне, воно було видалено відправником."; + +/* No comment provided by engineer. */ +"Group link" = "Посилання на групу"; + +/* No comment provided by engineer. */ +"Group links" = "Групові посилання"; + +/* No comment provided by engineer. */ +"Group members can add message reactions." = "Учасники групи можуть додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Group members can irreversibly delete sent messages." = "Учасники групи можуть безповоротно видаляти надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Group members can send direct messages." = "Учасники групи можуть надсилати прямі повідомлення."; + +/* No comment provided by engineer. */ +"Group members can send disappearing messages." = "Учасники групи можуть надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Group members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; + +/* No comment provided by engineer. */ +"Group members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; + +/* notification */ +"Group message:" = "Групове повідомлення:"; + +/* No comment provided by engineer. */ +"Group moderation" = "Модерація груп"; + +/* No comment provided by engineer. */ +"Group preferences" = "Параметри груп"; + +/* No comment provided by engineer. */ +"Group profile" = "Профіль групи"; + +/* No comment provided by engineer. */ +"Group profile is stored on members' devices, not on the servers." = "Профіль групи зберігається на пристроях учасників, а не на серверах."; + +/* snd group event chat item */ +"group profile updated" = "оновлено профіль групи"; + +/* No comment provided by engineer. */ +"Group welcome message" = "Привітальне повідомлення групи"; + +/* No comment provided by engineer. */ +"Group will be deleted for all members - this cannot be undone!" = "Група буде видалена для всіх учасників - це неможливо скасувати!"; + +/* No comment provided by engineer. */ +"Group will be deleted for you - this cannot be undone!" = "Група буде видалена для вас - це не може бути скасовано!"; + +/* No comment provided by engineer. */ +"Help" = "Довідка"; + +/* No comment provided by engineer. */ +"Hidden" = "Приховано"; + +/* No comment provided by engineer. */ +"Hidden chat profiles" = "Приховані профілі чату"; + +/* No comment provided by engineer. */ +"Hidden profile password" = "Прихований пароль до профілю"; + +/* chat item action */ +"Hide" = "Приховати"; + +/* No comment provided by engineer. */ +"Hide app screen in the recent apps." = "Приховати екран програми в останніх програмах."; + +/* No comment provided by engineer. */ +"Hide profile" = "Приховати профіль"; + +/* No comment provided by engineer. */ +"Hide:" = "Приховати:"; + +/* No comment provided by engineer. */ +"History" = "Історія"; + +/* time unit */ +"hours" = "години"; + +/* No comment provided by engineer. */ +"How it works" = "Як це працює"; + +/* No comment provided by engineer. */ +"How SimpleX works" = "Як працює SimpleX"; + +/* No comment provided by engineer. */ +"How to" = "Як зробити"; + +/* No comment provided by engineer. */ +"How to use it" = "Як ним користуватися"; + +/* No comment provided by engineer. */ +"How to use your servers" = "Як користуватися вашими серверами"; + +/* No comment provided by engineer. */ +"ICE servers (one per line)" = "Сервери ICE (по одному на лінію)"; + +/* No comment provided by engineer. */ +"If you can't meet in person, show QR code in a video call, or share the link." = "Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням."; + +/* No comment provided by engineer. */ +"If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link." = "Якщо ви не можете зустрітися особисто, ви можете **сканувати QR-код у відеодзвінку**, або ваш контакт може поділитися посиланням на запрошення."; + +/* No comment provided by engineer. */ +"If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Якщо ви введете цей пароль при відкритті програми, всі дані програми будуть безповоротно видалені!"; + +/* No comment provided by engineer. */ +"If you enter your self-destruct passcode while opening the app:" = "Якщо ви введете пароль самознищення під час відкриття програми:"; + +/* No comment provided by engineer. */ +"If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Якщо вам потрібно скористатися чатом зараз, натисніть **Зробити це пізніше** нижче (вам буде запропоновано перенести базу даних при перезапуску програми)."; + +/* No comment provided by engineer. */ +"Ignore" = "Ігнорувати"; + +/* No comment provided by engineer. */ +"Image will be received when your contact completes uploading it." = "Зображення буде отримано, коли ваш контакт завершить завантаження."; + +/* No comment provided by engineer. */ +"Image will be received when your contact is online, please wait or check later!" = "Зображення буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"Immediately" = "Негайно"; + +/* No comment provided by engineer. */ +"Immune to spam and abuse" = "Імунітет до спаму та зловживань"; + +/* No comment provided by engineer. */ +"Import" = "Імпорт"; + +/* No comment provided by engineer. */ +"Import chat database?" = "Імпортувати базу даних чату?"; + +/* No comment provided by engineer. */ +"Import database" = "Імпорт бази даних"; + +/* No comment provided by engineer. */ +"Improved privacy and security" = "Покращена конфіденційність та безпека"; + +/* No comment provided by engineer. */ +"Improved server configuration" = "Покращена конфігурація сервера"; + +/* No comment provided by engineer. */ +"In reply to" = "У відповідь на"; + +/* No comment provided by engineer. */ +"Incognito" = "Інкогніто"; + +/* No comment provided by engineer. */ +"Incognito mode" = "Режим інкогніто"; + +/* No comment provided by engineer. */ +"Incognito mode protects your privacy by using a new random profile for each contact." = "Режим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту."; + +/* chat list item description */ +"incognito via contact address link" = "інкогніто за посиланням на контактну адресу"; + +/* chat list item description */ +"incognito via group link" = "інкогніто через групове посилання"; + +/* chat list item description */ +"incognito via one-time link" = "інкогніто за одноразовим посиланням"; + +/* notification */ +"Incoming audio call" = "Вхідний аудіовиклик"; + +/* notification */ +"Incoming call" = "Вхідний дзвінок"; + +/* notification */ +"Incoming video call" = "Вхідний відеодзвінок"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несумісна версія бази даних"; + +/* PIN entry */ +"Incorrect passcode" = "Неправильний пароль"; + +/* No comment provided by engineer. */ +"Incorrect security code!" = "Неправильний код безпеки!"; + +/* connection level description */ +"indirect (%d)" = "непрямий (%d)"; + +/* chat item action */ +"Info" = "Інформація"; + +/* No comment provided by engineer. */ +"Initial role" = "Початкова роль"; + +/* No comment provided by engineer. */ +"Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)" = "Встановіть [SimpleX Chat для терміналу](https://github.com/simplex-chat/simplex-chat)"; + +/* No comment provided by engineer. */ +"Instant push notifications will be hidden!\n" = "Миттєві пуш-сповіщення будуть приховані!\n"; + +/* No comment provided by engineer. */ +"Instantly" = "Миттєво"; + +/* No comment provided by engineer. */ +"Interface" = "Інтерфейс"; + +/* invalid chat data */ +"invalid chat" = "недійсний чат"; + +/* No comment provided by engineer. */ +"invalid chat data" = "невірні дані чату"; + +/* No comment provided by engineer. */ +"Invalid connection link" = "Неправильне посилання для підключення"; + +/* invalid chat item */ +"invalid data" = "невірні дані"; + +/* No comment provided by engineer. */ +"Invalid server address!" = "Неправильна адреса сервера!"; + +/* item status text */ +"Invalid status" = "Недійсний статус"; + +/* No comment provided by engineer. */ +"Invitation expired!" = "Термін дії запрошення закінчився!"; + +/* group name */ +"invitation to group %@" = "запрошення до групи %@"; + +/* No comment provided by engineer. */ +"Invite friends" = "Запросити друзів"; + +/* No comment provided by engineer. */ +"Invite members" = "Запросити учасників"; + +/* No comment provided by engineer. */ +"Invite to group" = "Запросити до групи"; + +/* No comment provided by engineer. */ +"invited" = "запрошені"; + +/* rcv group event chat item */ +"invited %@" = "запрошений %@"; + +/* chat list item title */ +"invited to connect" = "запрошуємо приєднатися"; + +/* rcv group event chat item */ +"invited via your group link" = "запрошені за посиланням у вашій групі"; + +/* No comment provided by engineer. */ +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "iOS Keychain використовується для безпечного зберігання пароля - це дає змогу отримувати миттєві повідомлення."; + +/* No comment provided by engineer. */ +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення."; + +/* No comment provided by engineer. */ +"Irreversible message deletion" = "Безповоротне видалення повідомлення"; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this chat." = "У цьому чаті заборонено безповоротне видалення повідомлень."; + +/* No comment provided by engineer. */ +"Irreversible message deletion is prohibited in this group." = "У цій групі заборонено безповоротне видалення повідомлень."; + +/* No comment provided by engineer. */ +"It allows having many anonymous connections without any shared data between them in a single chat profile." = "Це дозволяє мати багато анонімних з'єднань без будь-яких спільних даних між ними в одному профілі чату."; + +/* No comment provided by engineer. */ +"It can happen when you or your connection used the old database backup." = "Це може статися, якщо ви або ваше з'єднання використовували стару резервну копію бази даних."; + +/* No comment provided by engineer. */ +"It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Це може статися, коли:\n1. Термін дії повідомлень закінчився в клієнті-відправнику через 2 дні або на сервері через 30 днів.\n2. Не вдалося розшифрувати повідомлення, тому що ви або ваш контакт використовували стару резервну копію бази даних.\n3. З'єднання було скомпрометовано."; + +/* No comment provided by engineer. */ +"It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@)."; + +/* No comment provided by engineer. */ +"Italian interface" = "Італійський інтерфейс"; + +/* No comment provided by engineer. */ +"italic" = "курсив"; + +/* No comment provided by engineer. */ +"Japanese interface" = "Японський інтерфейс"; + +/* No comment provided by engineer. */ +"Join" = "Приєднуйтесь"; + +/* No comment provided by engineer. */ +"join as %@" = "приєднатися як %@"; + +/* No comment provided by engineer. */ +"Join group" = "Приєднуйтесь до групи"; + +/* No comment provided by engineer. */ +"Join incognito" = "Приєднуйтесь інкогніто"; + +/* No comment provided by engineer. */ +"Joining group" = "Приєднання до групи"; + +/* No comment provided by engineer. */ +"Keep your connections" = "Зберігайте свої зв'язки"; + +/* No comment provided by engineer. */ +"Keychain error" = "помилка KeyChain"; + +/* No comment provided by engineer. */ +"KeyChain error" = "помилка KeyChain"; + +/* No comment provided by engineer. */ +"Large file!" = "Великий файл!"; + +/* No comment provided by engineer. */ +"Learn more" = "Дізнайтеся більше"; + +/* No comment provided by engineer. */ +"Leave" = "Залишити"; + +/* No comment provided by engineer. */ +"Leave group" = "Покинути групу"; + +/* No comment provided by engineer. */ +"Leave group?" = "Покинути групу?"; + +/* rcv group event chat item */ +"left" = "ліворуч"; + +/* email subject */ +"Let's talk in SimpleX Chat" = "Поговоримо в чаті SimpleX"; + +/* No comment provided by engineer. */ +"Light" = "Світлий"; + +/* No comment provided by engineer. */ +"Limitations" = "Обмеження"; + +/* No comment provided by engineer. */ +"LIVE" = "НАЖИВО"; + +/* No comment provided by engineer. */ +"Live message!" = "Живе повідомлення!"; + +/* No comment provided by engineer. */ +"Live messages" = "Живі повідомлення"; + +/* No comment provided by engineer. */ +"Local name" = "Місцева назва"; + +/* No comment provided by engineer. */ +"Local profile data only" = "Тільки локальні дані профілю"; + +/* No comment provided by engineer. */ +"Lock after" = "Блокування після"; + +/* No comment provided by engineer. */ +"Lock mode" = "Режим блокування"; + +/* No comment provided by engineer. */ +"Make a private connection" = "Створіть приватне з'єднання"; + +/* No comment provided by engineer. */ +"Make one message disappear" = "Зробити так, щоб одне повідомлення зникло"; + +/* No comment provided by engineer. */ +"Make profile private!" = "Зробіть профіль приватним!"; + +/* No comment provided by engineer. */ +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Переконайтеся, що адреси серверів %@ мають правильний формат, розділені рядками і не дублюються (%@)."; + +/* No comment provided by engineer. */ +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками і не дублюються."; + +/* No comment provided by engineer. */ +"Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Багато людей запитували: *якщо SimpleX не має ідентифікаторів користувачів, як він може доставляти повідомлення?*"; + +/* No comment provided by engineer. */ +"Mark deleted for everyone" = "Позначити видалено для всіх"; + +/* No comment provided by engineer. */ +"Mark read" = "Позначити прочитано"; + +/* No comment provided by engineer. */ +"Mark verified" = "Позначити перевірено"; + +/* No comment provided by engineer. */ +"Markdown in messages" = "Виправлення в повідомленнях"; + +/* marked deleted chat item preview text */ +"marked deleted" = "з позначкою видалено"; + +/* No comment provided by engineer. */ +"Max 30 seconds, received instantly." = "Максимум 30 секунд, отримується миттєво."; + +/* member role */ +"member" = "учасник"; + +/* No comment provided by engineer. */ +"Member" = "Учасник"; + +/* rcv group event chat item */ +"member connected" = "з'єднаний"; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". All group members will be notified." = "Роль учасника буде змінено на \"%@\". Всі учасники групи будуть повідомлені про це."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "Роль учасника буде змінено на \"%@\". Учасник отримає нове запрошення."; + +/* No comment provided by engineer. */ +"Member will be removed from group - this cannot be undone!" = "Учасник буде видалений з групи - це неможливо скасувати!"; + +/* item status text */ +"Message delivery error" = "Помилка доставки повідомлення"; + +/* No comment provided by engineer. */ +"Message delivery receipts!" = "Підтвердження доставки повідомлення!"; + +/* No comment provided by engineer. */ +"Message draft" = "Чернетка повідомлення"; + +/* chat feature */ +"Message reactions" = "Реакції на повідомлення"; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this chat." = "Реакції на повідомлення в цьому чаті заборонені."; + +/* No comment provided by engineer. */ +"Message reactions are prohibited in this group." = "Реакції на повідомлення в цій групі заборонені."; + +/* notification */ +"message received" = "повідомлення отримано"; + +/* No comment provided by engineer. */ +"Message text" = "Текст повідомлення"; + +/* No comment provided by engineer. */ +"Messages" = "Повідомлення"; + +/* No comment provided by engineer. */ +"Messages & files" = "Повідомлення та файли"; + +/* No comment provided by engineer. */ +"Migrating database archive…" = "Перенесення архіву бази даних…"; + +/* No comment provided by engineer. */ +"Migration error:" = "Помилка міграції:"; + +/* No comment provided by engineer. */ +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Міграція не вдалася. Натисніть **Пропустити** нижче, щоб продовжити використовувати поточну базу даних. Будь ласка, повідомте про проблему розробникам програми через чат або електронну пошту [chat@simplex.chat](mailto:chat@simplex.chat)."; + +/* No comment provided by engineer. */ +"Migration is completed" = "Міграцію завершено"; + +/* No comment provided by engineer. */ +"Migrations: %@" = "Міграції: %@"; + +/* time unit */ +"minutes" = "хвилини"; + +/* call status */ +"missed call" = "пропущений дзвінок"; + +/* chat item action */ +"Moderate" = "Модерується"; + +/* moderated chat item */ +"moderated" = "модерується"; + +/* No comment provided by engineer. */ +"Moderated at" = "Модерується на"; + +/* copied message info */ +"Moderated at: %@" = "Модерується за: %@"; + +/* No comment provided by engineer. */ +"moderated by %@" = "модерується %@"; + +/* time unit */ +"months" = "місяців"; + +/* No comment provided by engineer. */ +"More improvements are coming soon!" = "Незабаром буде ще більше покращень!"; + +/* item status description */ +"Most likely this connection is deleted." = "Швидше за все, це з'єднання видалено."; + +/* No comment provided by engineer. */ +"Most likely this contact has deleted the connection with you." = "Швидше за все, цей контакт видалив зв'язок з вами."; + +/* No comment provided by engineer. */ +"Multiple chat profiles" = "Кілька профілів чату"; + +/* No comment provided by engineer. */ +"Mute" = "Вимкнути звук"; + +/* No comment provided by engineer. */ +"Muted when inactive!" = "Вимкнено, коли неактивний!"; + +/* No comment provided by engineer. */ +"Name" = "Ім'я"; + +/* No comment provided by engineer. */ +"Network & servers" = "Мережа та сервери"; + +/* No comment provided by engineer. */ +"Network settings" = "Налаштування мережі"; + +/* No comment provided by engineer. */ +"Network status" = "Стан мережі"; + +/* No comment provided by engineer. */ +"never" = "ніколи"; + +/* notification */ +"New contact request" = "Новий запит на контакт"; + +/* notification */ +"New contact:" = "Новий контакт:"; + +/* No comment provided by engineer. */ +"New database archive" = "Новий архів бази даних"; + +/* No comment provided by engineer. */ +"New display name" = "Нове ім'я відображення"; + +/* No comment provided by engineer. */ +"New in %@" = "Нове в %@"; + +/* No comment provided by engineer. */ +"New member role" = "Нова роль учасника"; + +/* notification */ +"new message" = "нове повідомлення"; + +/* notification */ +"New message" = "Нове повідомлення"; + +/* No comment provided by engineer. */ +"New Passcode" = "Новий пароль"; + +/* No comment provided by engineer. */ +"New passphrase…" = "Новий пароль…"; + +/* pref value */ +"no" = "ні"; + +/* No comment provided by engineer. */ +"No" = "Ні"; + +/* Authentication unavailable */ +"No app password" = "Немає пароля програми"; + +/* No comment provided by engineer. */ +"No contacts selected" = "Не вибрано жодного контакту"; + +/* No comment provided by engineer. */ +"No contacts to add" = "Немає контактів для додавання"; + +/* No comment provided by engineer. */ +"No delivery information" = "Немає інформації про доставку"; + +/* No comment provided by engineer. */ +"No device token!" = "Токен пристрою відсутній!"; + +/* No comment provided by engineer. */ +"no e2e encryption" = "без шифрування e2e"; + +/* No comment provided by engineer. */ +"No filtered chats" = "Немає фільтрованих чатів"; + +/* No comment provided by engineer. */ +"No group!" = "Групу не знайдено!"; + +/* No comment provided by engineer. */ +"No history" = "Немає історії"; + +/* No comment provided by engineer. */ +"No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; + +/* No comment provided by engineer. */ +"No received or sent files" = "Немає отриманих або відправлених файлів"; + +/* copied message info in history */ +"no text" = "без тексту"; + +/* No comment provided by engineer. */ +"Notifications" = "Сповіщення"; + +/* No comment provided by engineer. */ +"Notifications are disabled!" = "Сповіщення вимкнено!"; + +/* No comment provided by engineer. */ +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Тепер адміністратори можуть\n- видаляти повідомлення користувачів.\n- відключати користувачів (роль \"спостерігач\")"; + +/* member role */ +"observer" = "спостерігач"; + +/* enabled status + group pref value */ +"off" = "вимкнено"; + +/* No comment provided by engineer. */ +"Off" = "Вимкнено"; + +/* No comment provided by engineer. */ +"Off (Local)" = "Вимкнено (локально)"; + +/* feature offered item */ +"offered %@" = "запропоновано %@"; + +/* feature offered item */ +"offered %@: %@" = "запропонував %1$@: %2$@"; + +/* No comment provided by engineer. */ +"Ok" = "Гаразд"; + +/* No comment provided by engineer. */ +"Old database" = "Стара база даних"; + +/* No comment provided by engineer. */ +"Old database archive" = "Старий архів бази даних"; + +/* group pref value */ +"on" = "увімкнено"; + +/* No comment provided by engineer. */ +"One-time invitation link" = "Посилання на одноразове запрошення"; + +/* No comment provided by engineer. */ +"Onion hosts will be required for connection. Requires enabling VPN." = "Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will be used when available. Requires enabling VPN." = "Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN."; + +/* No comment provided by engineer. */ +"Onion hosts will not be used." = "Onion хости не будуть використовуватися."; + +/* No comment provided by engineer. */ +"Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; + +/* No comment provided by engineer. */ +"Only group owners can change group preferences." = "Тільки власники груп можуть змінювати налаштування групи."; + +/* No comment provided by engineer. */ +"Only group owners can enable files and media." = "Тільки власники груп можуть вмикати файли та медіа."; + +/* No comment provided by engineer. */ +"Only group owners can enable voice messages." = "Тільки власники груп можуть вмикати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Only you can add message reactions." = "Тільки ви можете додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Only you can irreversibly delete messages (your contact can mark them for deletion)." = "Тільки ви можете безповоротно видалити повідомлення (ваш контакт може позначити їх для видалення)."; + +/* No comment provided by engineer. */ +"Only you can make calls." = "Дзвонити можете тільки ви."; + +/* No comment provided by engineer. */ +"Only you can send disappearing messages." = "Тільки ви можете надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Only you can send voice messages." = "Тільки ви можете надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Only your contact can add message reactions." = "Тільки ваш контакт може додавати реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Only your contact can irreversibly delete messages (you can mark them for deletion)." = "Тільки ваш контакт може безповоротно видалити повідомлення (ви можете позначити їх для видалення)."; + +/* No comment provided by engineer. */ +"Only your contact can make calls." = "Тільки ваш контакт може здійснювати дзвінки."; + +/* No comment provided by engineer. */ +"Only your contact can send disappearing messages." = "Тільки ваш контакт може надсилати зникаючі повідомлення."; + +/* No comment provided by engineer. */ +"Only your contact can send voice messages." = "Тільки ваш контакт може надсилати голосові повідомлення."; + +/* No comment provided by engineer. */ +"Open chat" = "Відкритий чат"; + +/* authentication reason */ +"Open chat console" = "Відкрийте консоль чату"; + +/* No comment provided by engineer. */ +"Open Settings" = "Відкрийте Налаштування"; + +/* authentication reason */ +"Open user profiles" = "Відкрити профілі користувачів"; + +/* No comment provided by engineer. */ +"Open-source protocol and code – anybody can run the servers." = "Протокол і код з відкритим вихідним кодом - будь-хто може запускати сервери."; + +/* No comment provided by engineer. */ +"Opening database…" = "Відкриття бази даних…"; + +/* No comment provided by engineer. */ +"Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red." = "Відкриття посилання в браузері може знизити конфіденційність і безпеку з'єднання. Ненадійні посилання SimpleX будуть червоного кольору."; + +/* No comment provided by engineer. */ +"or chat with the developers" = "або поспілкуйтеся з розробниками"; + +/* member role */ +"owner" = "власник"; + +/* No comment provided by engineer. */ +"Passcode" = "Пароль"; + +/* No comment provided by engineer. */ +"Passcode changed!" = "Пароль змінено!"; + +/* No comment provided by engineer. */ +"Passcode entry" = "Введення пароля"; + +/* No comment provided by engineer. */ +"Passcode not changed!" = "Пароль не змінено!"; + +/* No comment provided by engineer. */ +"Passcode set!" = "Пароль встановлено!"; + +/* No comment provided by engineer. */ +"Password to show" = "Показати пароль"; + +/* No comment provided by engineer. */ +"Paste" = "Вставити"; + +/* No comment provided by engineer. */ +"Paste image" = "Вставити зображення"; + +/* No comment provided by engineer. */ +"Paste received link" = "Вставте отримане посилання"; + +/* placeholder */ +"Paste the link you received to connect with your contact." = "Вставте отримане посилання для зв'язку з вашим контактом."; + +/* No comment provided by engineer. */ +"peer-to-peer" = "одноранговий"; + +/* No comment provided by engineer. */ +"People can connect to you only via the links you share." = "Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся."; + +/* No comment provided by engineer. */ +"Periodically" = "Періодично"; + +/* message decrypt error item */ +"Permanent decryption error" = "Постійна помилка розшифрування"; + +/* No comment provided by engineer. */ +"PING count" = "Кількість PING"; + +/* No comment provided by engineer. */ +"PING interval" = "Інтервал PING"; + +/* No comment provided by engineer. */ +"Please ask your contact to enable sending voice messages." = "Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень."; + +/* No comment provided by engineer. */ +"Please check that you used the correct link or ask your contact to send you another one." = "Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше."; + +/* No comment provided by engineer. */ +"Please check your network connection with %@ and try again." = "Будь ласка, перевірте підключення до мережі за допомогою %@ і спробуйте ще раз."; + +/* No comment provided by engineer. */ +"Please check yours and your contact preferences." = "Будь ласка, перевірте свої та контактні налаштування."; + +/* No comment provided by engineer. */ +"Please contact group admin." = "Зверніться до адміністратора групи."; + +/* No comment provided by engineer. */ +"Please enter correct current passphrase." = "Будь ласка, введіть правильний поточний пароль."; + +/* No comment provided by engineer. */ +"Please enter the previous password after restoring database backup. This action can not be undone." = "Будь ласка, введіть попередній пароль після відновлення резервної копії бази даних. Ця дія не може бути скасована."; + +/* No comment provided by engineer. */ +"Please remember or store it securely - there is no way to recover a lost passcode!" = "Будь ласка, запам'ятайте або надійно зберігайте його - втрачений пароль неможливо відновити!"; + +/* No comment provided by engineer. */ +"Please report it to the developers." = "Будь ласка, повідомте про це розробникам."; + +/* No comment provided by engineer. */ +"Please restart the app and migrate the database to enable push notifications." = "Будь ласка, перезапустіть додаток і перенесіть базу даних, щоб увімкнути push-сповіщення."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to access chat if you lose it." = "Будь ласка, зберігайте пароль надійно, ви НЕ зможете отримати доступ до чату, якщо втратите його."; + +/* No comment provided by engineer. */ +"Please store passphrase securely, you will NOT be able to change it if you lose it." = "Будь ласка, зберігайте пароль надійно, ви НЕ зможете змінити його, якщо втратите."; + +/* No comment provided by engineer. */ +"Polish interface" = "Польський інтерфейс"; + +/* server test error */ +"Possibly, certificate fingerprint in server address is incorrect" = "Можливо, в адресі сервера неправильно вказано відбиток сертифіката"; + +/* No comment provided by engineer. */ +"Preserve the last message draft, with attachments." = "Зберегти чернетку останнього повідомлення з вкладеннями."; + +/* No comment provided by engineer. */ +"Preset server" = "Попередньо встановлений сервер"; + +/* No comment provided by engineer. */ +"Preset server address" = "Попередньо встановлена адреса сервера"; + +/* No comment provided by engineer. */ +"Preview" = "Попередній перегляд"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Конфіденційність і безпека"; + +/* No comment provided by engineer. */ +"Privacy redefined" = "Конфіденційність переглянута"; + +/* No comment provided by engineer. */ +"Private filenames" = "Приватні імена файлів"; + +/* No comment provided by engineer. */ +"Profile and server connections" = "З'єднання профілю та сервера"; + +/* No comment provided by engineer. */ +"Profile image" = "Зображення профілю"; + +/* No comment provided by engineer. */ +"Profile password" = "Пароль до профілю"; + +/* No comment provided by engineer. */ +"Profile update will be sent to your contacts." = "Оновлення профілю буде надіслано вашим контактам."; + +/* No comment provided by engineer. */ +"Prohibit audio/video calls." = "Заборонити аудіо/відеодзвінки."; + +/* No comment provided by engineer. */ +"Prohibit irreversible message deletion." = "Заборонити незворотне видалення повідомлень."; + +/* No comment provided by engineer. */ +"Prohibit message reactions." = "Заборонити реакцію на повідомлення."; + +/* No comment provided by engineer. */ +"Prohibit messages reactions." = "Заборонити реакції на повідомлення."; + +/* No comment provided by engineer. */ +"Prohibit sending direct messages to members." = "Заборонити надсилати прямі повідомлення учасникам."; + +/* No comment provided by engineer. */ +"Prohibit sending disappearing messages." = "Заборонити надсилання зникаючих повідомлень."; + +/* No comment provided by engineer. */ +"Prohibit sending files and media." = "Заборонити надсилання файлів і медіа."; + +/* No comment provided by engineer. */ +"Prohibit sending voice messages." = "Заборонити надсилання голосових повідомлень."; + +/* No comment provided by engineer. */ +"Protect app screen" = "Захистіть екран програми"; + +/* No comment provided by engineer. */ +"Protect your chat profiles with a password!" = "Захистіть свої профілі чату паролем!"; + +/* No comment provided by engineer. */ +"Protocol timeout" = "Тайм-аут протоколу"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Тайм-аут протоколу на КБ"; + +/* No comment provided by engineer. */ +"Push notifications" = "Push-повідомлення"; + +/* No comment provided by engineer. */ +"Rate the app" = "Оцініть додаток"; + +/* chat item menu */ +"React…" = "Реагуй…"; + +/* No comment provided by engineer. */ +"Read" = "Читати"; + +/* No comment provided by engineer. */ +"Read more" = "Читати далі"; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; + +/* No comment provided by engineer. */ +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Читайте більше в [Посібнику користувача](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; + +/* No comment provided by engineer. */ +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Читайте більше в нашому [GitHub репозиторії](https://github.com/simplex-chat/simplex-chat#readme)."; + +/* No comment provided by engineer. */ +"Read more in our GitHub repository." = "Читайте більше в нашому репозиторії на GitHub."; + +/* No comment provided by engineer. */ +"Receipts are disabled" = "Підтвердження виключені"; + +/* No comment provided by engineer. */ +"received answer…" = "отримали відповідь…"; + +/* No comment provided by engineer. */ +"Received at" = "Отримано за"; + +/* copied message info */ +"Received at: %@" = "Отримано за: %@"; + +/* No comment provided by engineer. */ +"received confirmation…" = "отримали підтвердження…"; + +/* notification */ +"Received file event" = "Подія отримання файлу"; + +/* message info title */ +"Received message" = "Отримано повідомлення"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі."; + +/* No comment provided by engineer. */ +"Receiving file will be stopped." = "Отримання файлу буде зупинено."; + +/* No comment provided by engineer. */ +"Receiving via" = "Отримання через"; + +/* No comment provided by engineer. */ +"Recipients see updates as you type them." = "Одержувачі бачать оновлення, коли ви їх вводите."; + +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік."; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Перепідключити сервери?"; + +/* No comment provided by engineer. */ +"Record updated at" = "Запис оновлено за"; + +/* copied message info */ +"Record updated at: %@" = "Запис оновлено за: %@"; + +/* No comment provided by engineer. */ +"Reduced battery usage" = "Зменшення використання акумулятора"; + +/* reject incoming call via notification */ +"Reject" = "Відхилити"; + +/* No comment provided by engineer. */ +"Reject (sender NOT notified)" = "Відхилити (відправника НЕ повідомлено)"; + +/* No comment provided by engineer. */ +"Reject contact request" = "Відхилити запит на контакт"; + +/* call status */ +"rejected call" = "відхилений виклик"; + +/* No comment provided by engineer. */ +"Relay server is only used if necessary. Another party can observe your IP address." = "Релейний сервер використовується тільки в разі потреби. Інша сторона може бачити вашу IP-адресу."; + +/* No comment provided by engineer. */ +"Relay server protects your IP address, but it can observe the duration of the call." = "Сервер ретрансляції захищає вашу IP-адресу, але він може спостерігати за тривалістю дзвінка."; + +/* No comment provided by engineer. */ +"Remove" = "Видалити"; + +/* No comment provided by engineer. */ +"Remove member" = "Видалити учасника"; + +/* No comment provided by engineer. */ +"Remove member?" = "Видалити учасника?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Видалити парольну фразу з брелока?"; + +/* No comment provided by engineer. */ +"removed" = "видалено"; + +/* rcv group event chat item */ +"removed %@" = "видалено %@"; + +/* rcv group event chat item */ +"removed you" = "прибрали вас"; + +/* No comment provided by engineer. */ +"Renegotiate" = "Переузгодьте"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "Переузгодьте шифрування"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "Переузгодьте шифрування?"; + +/* chat item action */ +"Reply" = "Відповісти"; + +/* No comment provided by engineer. */ +"Required" = "Потрібно"; + +/* No comment provided by engineer. */ +"Reset" = "Перезавантаження"; + +/* No comment provided by engineer. */ +"Reset colors" = "Скинути кольори"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Відновити налаштування за замовчуванням"; + +/* No comment provided by engineer. */ +"Restart the app to create a new chat profile" = "Перезапустіть програму, щоб створити новий профіль чату"; + +/* No comment provided by engineer. */ +"Restart the app to use imported chat database" = "Перезапустіть програму, щоб використовувати імпортовану базу даних чату"; + +/* No comment provided by engineer. */ +"Restore" = "Відновити"; + +/* No comment provided by engineer. */ +"Restore database backup" = "Відновлення резервної копії бази даних"; + +/* No comment provided by engineer. */ +"Restore database backup?" = "Відновити резервну копію бази даних?"; + +/* No comment provided by engineer. */ +"Restore database error" = "Відновлення помилки бази даних"; + +/* chat item action */ +"Reveal" = "Показувати"; + +/* No comment provided by engineer. */ +"Revert" = "Повернутися"; + +/* No comment provided by engineer. */ +"Revoke" = "Відкликати"; + +/* cancel file action */ +"Revoke file" = "Відкликати файл"; + +/* No comment provided by engineer. */ +"Revoke file?" = "Відкликати файл?"; + +/* No comment provided by engineer. */ +"Role" = "Роль"; + +/* No comment provided by engineer. */ +"Run chat" = "Запустити чат"; + +/* chat item action */ +"Save" = "Зберегти"; + +/* No comment provided by engineer. */ +"Save (and notify contacts)" = "Зберегти (і повідомити контактам)"; + +/* No comment provided by engineer. */ +"Save and notify contact" = "Зберегти та повідомити контакт"; + +/* No comment provided by engineer. */ +"Save and notify group members" = "Зберегти та повідомити учасників групи"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Збереження та оновлення профілю групи"; + +/* No comment provided by engineer. */ +"Save archive" = "Зберегти архів"; + +/* No comment provided by engineer. */ +"Save auto-accept settings" = "Зберегти налаштування автоприйому"; + +/* No comment provided by engineer. */ +"Save group profile" = "Зберегти профіль групи"; + +/* No comment provided by engineer. */ +"Save passphrase and open chat" = "Збережіть пароль і відкрийте чат"; + +/* No comment provided by engineer. */ +"Save passphrase in Keychain" = "Збережіть парольну фразу в Keychain"; + +/* No comment provided by engineer. */ +"Save preferences?" = "Зберегти налаштування?"; + +/* No comment provided by engineer. */ +"Save profile password" = "Зберегти пароль профілю"; + +/* No comment provided by engineer. */ +"Save servers" = "Зберегти сервери"; + +/* No comment provided by engineer. */ +"Save servers?" = "Зберегти сервери?"; + +/* No comment provided by engineer. */ +"Save settings?" = "Зберегти налаштування?"; + +/* No comment provided by engineer. */ +"Save welcome message?" = "Зберегти вітальне повідомлення?"; + +/* No comment provided by engineer. */ +"Saved WebRTC ICE servers will be removed" = "Збережені сервери WebRTC ICE буде видалено"; + +/* No comment provided by engineer. */ +"Scan code" = "Сканувати код"; + +/* No comment provided by engineer. */ +"Scan QR code" = "Відскануйте QR-код"; + +/* No comment provided by engineer. */ +"Scan security code from your contact's app." = "Відскануйте код безпеки з додатку вашого контакту."; + +/* No comment provided by engineer. */ +"Scan server QR code" = "Відскануйте QR-код сервера"; + +/* No comment provided by engineer. */ +"Search" = "Пошук"; + +/* network option */ +"sec" = "сек"; + +/* time unit */ +"seconds" = "секунди"; + +/* No comment provided by engineer. */ +"secret" = "таємниця"; + +/* server test step */ +"Secure queue" = "Безпечна черга"; + +/* No comment provided by engineer. */ +"Security assessment" = "Оцінка безпеки"; + +/* No comment provided by engineer. */ +"Security code" = "Код безпеки"; + +/* chat item text */ +"security code changed" = "змінено код безпеки"; + +/* No comment provided by engineer. */ +"Select" = "Виберіть"; + +/* No comment provided by engineer. */ +"Self-destruct" = "Самознищення"; + +/* No comment provided by engineer. */ +"Self-destruct passcode" = "Пароль самознищення"; + +/* No comment provided by engineer. */ +"Self-destruct passcode changed!" = "Пароль самознищення змінено!"; + +/* No comment provided by engineer. */ +"Self-destruct passcode enabled!" = "Пароль самознищення ввімкнено!"; + +/* No comment provided by engineer. */ +"Send" = "Надіслати"; + +/* No comment provided by engineer. */ +"Send a live message - it will update for the recipient(s) as you type it" = "Надішліть повідомлення в реальному часі - воно буде оновлюватися для одержувача (одержувачів), поки ви його вводите"; + +/* No comment provided by engineer. */ +"Send delivery receipts to" = "Надсилання звітів про доставку"; + +/* No comment provided by engineer. */ +"Send direct message" = "Надішліть пряме повідомлення"; + +/* No comment provided by engineer. */ +"Send disappearing message" = "Надіслати зникаюче повідомлення"; + +/* No comment provided by engineer. */ +"Send link previews" = "Надіслати попередній перегляд за посиланням"; + +/* No comment provided by engineer. */ +"Send live message" = "Надіслати живе повідомлення"; + +/* No comment provided by engineer. */ +"Send notifications" = "Надсилати сповіщення"; + +/* No comment provided by engineer. */ +"Send notifications:" = "Надсилати сповіщення:"; + +/* No comment provided by engineer. */ +"Send questions and ideas" = "Надсилайте запитання та ідеї"; + +/* No comment provided by engineer. */ +"Send receipts" = "Надіслати підтвердження"; + +/* No comment provided by engineer. */ +"Send them from gallery or custom keyboards." = "Надсилайте їх із галереї чи власних клавіатур."; + +/* No comment provided by engineer. */ +"Sender cancelled file transfer." = "Відправник скасував передачу файлу."; + +/* No comment provided by engineer. */ +"Sender may have deleted the connection request." = "Можливо, відправник видалив запит на підключення."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "Надсилання підтверджень доставки буде ввімкнено для всіх контактів у всіх видимих профілях чату."; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "Надсилання підтверджень доставки буде ввімкнено для всіх контактів."; + +/* No comment provided by engineer. */ +"Sending file will be stopped." = "Надсилання файлу буде зупинено."; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "Надсилання підтвердження вимкнено для контактів %lld"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "Відправлення підтверджень вимкнено для груп %lld"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "Для контактів %lld увімкнено надсилання підтвердження"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "Для груп %lld увімкнено надсилання підтвердження"; + +/* No comment provided by engineer. */ +"Sending via" = "Надсилання через"; + +/* No comment provided by engineer. */ +"Sent at" = "Надіслано за"; + +/* copied message info */ +"Sent at: %@" = "Надіслано за: %@"; + +/* notification */ +"Sent file event" = "Подія надісланого файлу"; + +/* message info title */ +"Sent message" = "Надіслано повідомлення"; + +/* No comment provided by engineer. */ +"Sent messages will be deleted after set time." = "Надіслані повідомлення будуть видалені через встановлений час."; + +/* server test error */ +"Server requires authorization to create queues, check password" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; + +/* server test error */ +"Server requires authorization to upload, check password" = "Сервер вимагає авторизації для завантаження, перевірте пароль"; + +/* No comment provided by engineer. */ +"Server test failed!" = "Тест сервера завершився невдало!"; + +/* No comment provided by engineer. */ +"Servers" = "Сервери"; + +/* No comment provided by engineer. */ +"Set 1 day" = "Встановити 1 день"; + +/* No comment provided by engineer. */ +"Set contact name…" = "Встановити ім'я контакту…"; + +/* No comment provided by engineer. */ +"Set group preferences" = "Встановіть налаштування групи"; + +/* No comment provided by engineer. */ +"Set it instead of system authentication." = "Встановіть його замість аутентифікації системи."; + +/* No comment provided by engineer. */ +"Set passcode" = "Встановити пароль"; + +/* No comment provided by engineer. */ +"Set passphrase to export" = "Встановити ключову фразу для експорту"; + +/* No comment provided by engineer. */ +"Set the message shown to new members!" = "Налаштуйте повідомлення, яке показуватиметься новим користувачам!"; + +/* No comment provided by engineer. */ +"Set timeouts for proxy/VPN" = "Встановлення таймаутів для проксі/VPN"; + +/* No comment provided by engineer. */ +"Settings" = "Налаштування"; + +/* chat item action */ +"Share" = "Поділіться"; + +/* No comment provided by engineer. */ +"Share 1-time link" = "Поділитися 1-разовим посиланням"; + +/* No comment provided by engineer. */ +"Share address" = "Поділитися адресою"; + +/* No comment provided by engineer. */ +"Share address with contacts?" = "Поділіться адресою з контактами?"; + +/* No comment provided by engineer. */ +"Share link" = "Поділіться посиланням"; + +/* No comment provided by engineer. */ +"Share one-time invitation link" = "Поділіться посиланням на одноразове запрошення"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Поділіться з контактами"; + +/* No comment provided by engineer. */ +"Show calls in phone history" = "Показувати дзвінки в історії дзвінків"; + +/* No comment provided by engineer. */ +"Show developer options" = "Показати опції розробника"; + +/* No comment provided by engineer. */ +"Show last messages" = "Показати останні повідомлення"; + +/* No comment provided by engineer. */ +"Show preview" = "Показати попередній перегляд"; + +/* No comment provided by engineer. */ +"Show:" = "Показати:"; + +/* No comment provided by engineer. */ +"SimpleX address" = "Адреса SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "Адреса SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Chat security was audited by Trail of Bits." = "Безпека SimpleX Chat була перевірена компанією Trail of Bits."; + +/* simplex link type */ +"SimpleX contact address" = "Контактна адреса SimpleX"; + +/* notification */ +"SimpleX encrypted message or connection event" = "Зашифроване повідомлення SimpleX або подія підключення"; + +/* simplex link type */ +"SimpleX group link" = "Посилання на групу SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX links" = "Посилання SimpleX"; + +/* No comment provided by engineer. */ +"SimpleX Lock" = "SimpleX Lock"; + +/* No comment provided by engineer. */ +"SimpleX Lock mode" = "Режим SimpleX Lock"; + +/* No comment provided by engineer. */ +"SimpleX Lock not enabled!" = "SimpleX Lock не ввімкнено!"; + +/* No comment provided by engineer. */ +"SimpleX Lock turned on" = "SimpleX Lock увімкнено"; + +/* simplex link type */ +"SimpleX one-time invitation" = "Одноразове запрошення SimpleX"; + +/* No comment provided by engineer. */ +"Skip" = "Пропустити"; + +/* No comment provided by engineer. */ +"Skipped messages" = "Пропущені повідомлення"; + +/* No comment provided by engineer. */ +"Small groups (max 20)" = "Невеликі групи (максимум 20 осіб)"; + +/* No comment provided by engineer. */ +"SMP servers" = "Сервери SMP"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше."; + +/* notification title */ +"Somebody" = "Хтось"; + +/* No comment provided by engineer. */ +"Start a new chat" = "Почніть новий чат"; + +/* No comment provided by engineer. */ +"Start chat" = "Почати чат"; + +/* No comment provided by engineer. */ +"Start migration" = "Почати міграцію"; + +/* No comment provided by engineer. */ +"starting…" = "починаючи…"; + +/* No comment provided by engineer. */ +"Stop" = "Зупинити"; + +/* No comment provided by engineer. */ +"Stop chat to enable database actions" = "Зупиніть чат, щоб увімкнути дії з базою даних"; + +/* No comment provided by engineer. */ +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Зупиніть чат, щоб експортувати, імпортувати або видалити базу даних чату. Ви не зможете отримувати та надсилати повідомлення, поки чат зупинено."; + +/* No comment provided by engineer. */ +"Stop chat?" = "Зупинити чат?"; + +/* cancel file action */ +"Stop file" = "Зупинити файл"; + +/* No comment provided by engineer. */ +"Stop receiving file?" = "Припинити отримання файлу?"; + +/* No comment provided by engineer. */ +"Stop sending file?" = "Припинити надсилання файлу?"; + +/* No comment provided by engineer. */ +"Stop sharing" = "Припиніть ділитися"; + +/* No comment provided by engineer. */ +"Stop sharing address?" = "Припинити ділитися адресою?"; + +/* authentication reason */ +"Stop SimpleX" = "Зупинити SimpleX"; + +/* No comment provided by engineer. */ +"strike" = "закреслено"; + +/* No comment provided by engineer. */ +"Submit" = "Надіслати"; + +/* No comment provided by engineer. */ +"Support SimpleX Chat" = "Підтримка чату SimpleX"; + +/* No comment provided by engineer. */ +"System" = "Система"; + +/* No comment provided by engineer. */ +"System authentication" = "Автентифікація системи"; + +/* No comment provided by engineer. */ +"Take picture" = "Сфотографуйте"; + +/* No comment provided by engineer. */ +"Tap button " = "Натисніть кнопку "; + +/* No comment provided by engineer. */ +"Tap to activate profile." = "Натисніть, щоб активувати профіль."; + +/* No comment provided by engineer. */ +"Tap to join" = "Натисніть, щоб приєднатися"; + +/* No comment provided by engineer. */ +"Tap to join incognito" = "Натисніть, щоб приєднатися інкогніто"; + +/* No comment provided by engineer. */ +"Tap to start a new chat" = "Натисніть, щоб почати новий чат"; + +/* No comment provided by engineer. */ +"TCP connection timeout" = "Тайм-аут TCP-з'єднання"; + +/* No comment provided by engineer. */ +"TCP_KEEPCNT" = "TCP_KEEPCNT"; + +/* No comment provided by engineer. */ +"TCP_KEEPIDLE" = "TCP_KEEPIDLE"; + +/* No comment provided by engineer. */ +"TCP_KEEPINTVL" = "TCP_KEEPINTVL"; + +/* server test failure */ +"Test failed at step %@." = "Тест завершився невдало на кроці %@."; + +/* No comment provided by engineer. */ +"Test server" = "Тестовий сервер"; + +/* No comment provided by engineer. */ +"Test servers" = "Тестові сервери"; + +/* No comment provided by engineer. */ +"Tests failed!" = "Тести не пройшли!"; + +/* No comment provided by engineer. */ +"Thank you for installing SimpleX Chat!" = "Дякуємо, що встановили SimpleX Chat!"; + +/* No comment provided by engineer. */ +"Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Дякуємо користувачам - [внесок через Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; + +/* No comment provided by engineer. */ +"Thanks to the users – contribute via Weblate!" = "Дякуємо користувачам - зробіть свій внесок через Weblate!"; + +/* No comment provided by engineer. */ +"The 1st platform without any user identifiers – private by design." = "Перша платформа без жодних ідентифікаторів користувачів – приватна за дизайном."; + +/* No comment provided by engineer. */ +"The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; + +/* No comment provided by engineer. */ +"The attempt to change database passphrase was not completed." = "Спроба змінити пароль до бази даних не була завершена."; + +/* No comment provided by engineer. */ +"The connection you accepted will be cancelled!" = "Прийняте вами з'єднання буде скасовано!"; + +/* No comment provided by engineer. */ +"The contact you shared this link with will NOT be able to connect!" = "Контакт, з яким ви поділилися цим посиланням, НЕ зможе підключитися!"; + +/* No comment provided by engineer. */ +"The created archive is available via app Settings / Database / Old database archive." = "Створений архів доступний через Налаштування програми / База даних / Старий архів бази даних."; + +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з'єднання!"; + +/* No comment provided by engineer. */ +"The group is fully decentralized – it is visible only to the members." = "Група повністю децентралізована - її бачать лише учасники."; + +/* No comment provided by engineer. */ +"The hash of the previous message is different." = "Хеш попереднього повідомлення відрізняється."; + +/* No comment provided by engineer. */ +"The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "Ідентифікатор наступного повідомлення неправильний (менше або дорівнює попередньому).\nЦе може статися через помилку або коли з'єднання скомпрометовано."; + +/* No comment provided by engineer. */ +"The message will be deleted for all members." = "Повідомлення буде видалено для всіх учасників."; + +/* No comment provided by engineer. */ +"The message will be marked as moderated for all members." = "Повідомлення буде позначено як модероване для всіх учасників."; + +/* No comment provided by engineer. */ +"The next generation of private messaging" = "Наступне покоління приватних повідомлень"; + +/* No comment provided by engineer. */ +"The old database was not removed during the migration, it can be deleted." = "Стара база даних не була видалена під час міграції, її можна видалити."; + +/* No comment provided by engineer. */ +"The profile is only shared with your contacts." = "Профіль доступний лише вашим контактам."; + +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "Другу галочку ми пропустили! ✅"; + +/* No comment provided by engineer. */ +"The sender will NOT be notified" = "Відправник НЕ буде повідомлений"; + +/* No comment provided by engineer. */ +"The servers for new connections of your current chat profile **%@**." = "Сервери для нових підключень вашого поточного профілю чату **%@**."; + +/* No comment provided by engineer. */ +"Theme" = "Тема"; + +/* No comment provided by engineer. */ +"There should be at least one user profile." = "Повинен бути принаймні один профіль користувача."; + +/* No comment provided by engineer. */ +"There should be at least one visible user profile." = "Повинен бути принаймні один видимий профіль користувача."; + +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "Ці налаштування стосуються вашого поточного профілю **%@**."; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "Їх можна перевизначити в налаштуваннях контактів і груп."; + +/* No comment provided by engineer. */ +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Цю дію неможливо скасувати - всі отримані та надіслані файли і медіа будуть видалені. Зображення з низькою роздільною здатністю залишаться."; + +/* No comment provided by engineer. */ +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Цю дію неможливо скасувати - повідомлення, надіслані та отримані раніше, ніж вибрані, будуть видалені. Це може зайняти кілька хвилин."; + +/* No comment provided by engineer. */ +"This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Цю дію неможливо скасувати - ваш профіль, контакти, повідомлення та файли будуть безповоротно втрачені."; + +/* notification title */ +"this contact" = "цей контакт"; + +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "У цій групі більше %lld учасників, підтвердження доставки не надсилаються."; + +/* No comment provided by engineer. */ +"This group no longer exists." = "Цієї групи більше не існує."; + +/* No comment provided by engineer. */ +"This setting applies to messages in your current chat profile **%@**." = "Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**."; + +/* No comment provided by engineer. */ +"To ask any questions and to receive updates:" = "Задати будь-які питання та отримувати новини:"; + +/* No comment provided by engineer. */ +"To connect, your contact can scan QR code or use the link in the app." = "Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку."; + +/* No comment provided by engineer. */ +"To make a new connection" = "Щоб створити нове з'єднання"; + +/* No comment provided by engineer. */ +"To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts." = "Щоб захистити конфіденційність, замість ідентифікаторів користувачів, які використовуються на всіх інших платформах, SimpleX має ідентифікатори для черг повідомлень, окремі для кожного з ваших контактів."; + +/* No comment provided by engineer. */ +"To protect timezone, image/voice files use UTC." = "Для захисту часового поясу у файлах зображень/голосу використовується UTC."; + +/* No comment provided by engineer. */ +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Щоб захистити вашу інформацію, увімкніть SimpleX Lock.\nПеред увімкненням цієї функції вам буде запропоновано пройти автентифікацію."; + +/* No comment provided by engineer. */ +"To record voice message please grant permission to use Microphone." = "Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону."; + +/* No comment provided by engineer. */ +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Щоб відкрити свій прихований профіль, введіть повний пароль у поле пошуку на сторінці **Ваші профілі чату**."; + +/* No comment provided by engineer. */ +"To support instant push notifications the chat database has to be migrated." = "Для підтримки миттєвих push-повідомлень необхідно перенести базу даних чату."; + +/* No comment provided by engineer. */ +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях."; + +/* No comment provided by engineer. */ +"Transport isolation" = "Транспортна ізоляція"; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact (error: %@)." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@)."; + +/* No comment provided by engineer. */ +"Trying to connect to the server used to receive messages from this contact." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту."; + +/* No comment provided by engineer. */ +"Turn off" = "Вимкнути"; + +/* No comment provided by engineer. */ +"Turn off notifications?" = "Вимкнути сповіщення?"; + +/* No comment provided by engineer. */ +"Turn on" = "Ввімкнути"; + +/* No comment provided by engineer. */ +"Unable to record voice message" = "Не вдається записати голосове повідомлення"; + +/* item status description */ +"Unexpected error: %@" = "Неочікувана помилка: %@"; + +/* No comment provided by engineer. */ +"Unexpected migration state" = "Неочікуваний стан міграції"; + +/* No comment provided by engineer. */ +"Unfav." = "Нелюб."; + +/* No comment provided by engineer. */ +"Unhide" = "Показати"; + +/* No comment provided by engineer. */ +"Unhide chat profile" = "Показати профіль чату"; + +/* No comment provided by engineer. */ +"Unhide profile" = "Показати профіль"; + +/* No comment provided by engineer. */ +"Unit" = "Одиниця"; + +/* connection info */ +"unknown" = "невідомий"; + +/* callkit banner */ +"Unknown caller" = "Невідомий абонент"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Невідома помилка бази даних: %@"; + +/* No comment provided by engineer. */ +"Unknown error" = "Невідома помилка"; + +/* No comment provided by engineer. */ +"Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим \"Не турбувати\", щоб уникнути переривань."; + +/* No comment provided by engineer. */ +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "Якщо ваш контакт не видалив з'єднання або якщо це посилання вже використовувалося, це може бути помилкою - будь ласка, повідомте про це.\nЩоб підключитися, попросіть вашого контакта створити інше посилання і перевірте, чи маєте ви стабільне з'єднання з мережею."; + +/* No comment provided by engineer. */ +"Unlock" = "Розблокувати"; + +/* authentication reason */ +"Unlock app" = "Розблокувати додаток"; + +/* No comment provided by engineer. */ +"Unmute" = "Увімкнути звук"; + +/* No comment provided by engineer. */ +"Unread" = "Непрочитане"; + +/* No comment provided by engineer. */ +"Update" = "Оновлення"; + +/* No comment provided by engineer. */ +"Update .onion hosts setting?" = "Оновити налаштування хостів .onion?"; + +/* No comment provided by engineer. */ +"Update database passphrase" = "Оновити парольну фразу бази даних"; + +/* No comment provided by engineer. */ +"Update network settings?" = "Оновити налаштування мережі?"; + +/* No comment provided by engineer. */ +"Update transport isolation mode?" = "Оновити режим транспортної ізоляції?"; + +/* rcv group event chat item */ +"updated group profile" = "оновлений профіль групи"; + +/* No comment provided by engineer. */ +"Updating settings will re-connect the client to all servers." = "Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів."; + +/* No comment provided by engineer. */ +"Updating this setting will re-connect the client to all servers." = "Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів."; + +/* No comment provided by engineer. */ +"Upgrade and open chat" = "Оновлення та відкритий чат"; + +/* server test step */ +"Upload file" = "Завантажити файл"; + +/* No comment provided by engineer. */ +"Use .onion hosts" = "Використовуйте хости .onion"; + +/* No comment provided by engineer. */ +"Use chat" = "Використовуйте чат"; + +/* No comment provided by engineer. */ +"Use current profile" = "Використовувати поточний профіль"; + +/* No comment provided by engineer. */ +"Use for new connections" = "Використовуйте для нових з'єднань"; + +/* No comment provided by engineer. */ +"Use iOS call interface" = "Використовуйте інтерфейс виклику iOS"; + +/* No comment provided by engineer. */ +"Use new incognito profile" = "Використовуйте новий профіль інкогніто"; + +/* No comment provided by engineer. */ +"Use server" = "Використовувати сервер"; + +/* No comment provided by engineer. */ +"Use SimpleX Chat servers?" = "Використовувати сервери SimpleX Chat?"; + +/* No comment provided by engineer. */ +"User profile" = "Профіль користувача"; + +/* No comment provided by engineer. */ +"Using .onion hosts requires compatible VPN provider." = "Для використання хостів .onion потрібен сумісний VPN-провайдер."; + +/* No comment provided by engineer. */ +"Using SimpleX Chat servers." = "Використання серверів SimpleX Chat."; + +/* No comment provided by engineer. */ +"v%@ (%@)" = "v%@ (%@)"; + +/* No comment provided by engineer. */ +"Verify connection security" = "Перевірте безпеку з'єднання"; + +/* No comment provided by engineer. */ +"Verify security code" = "Підтвердіть код безпеки"; + +/* No comment provided by engineer. */ +"Via browser" = "Через браузер"; + +/* chat list item description */ +"via contact address link" = "за посиланням на контактну адресу"; + +/* chat list item description */ +"via group link" = "за посиланням на групу"; + +/* chat list item description */ +"via one-time link" = "за одноразовим посиланням"; + +/* No comment provided by engineer. */ +"via relay" = "за допомогою ретранслятора"; + +/* No comment provided by engineer. */ +"Video call" = "Відеодзвінок"; + +/* No comment provided by engineer. */ +"video call (not e2e encrypted)" = "відеодзвінок (без шифрування e2e)"; + +/* No comment provided by engineer. */ +"Video will be received when your contact completes uploading it." = "Відео буде отримано, коли ваш контакт завершить завантаження."; + +/* No comment provided by engineer. */ +"Video will be received when your contact is online, please wait or check later!" = "Відео буде отримано, коли ваш контакт буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"Videos and files up to 1gb" = "Відео та файли до 1 Гб"; + +/* No comment provided by engineer. */ +"View security code" = "Переглянути код безпеки"; + +/* No comment provided by engineer. */ +"Voice message…" = "Голосове повідомлення…"; + +/* chat feature */ +"Voice messages" = "Голосові повідомлення"; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this chat." = "Голосові повідомлення в цьому чаті заборонені."; + +/* No comment provided by engineer. */ +"Voice messages are prohibited in this group." = "Голосові повідомлення в цій групі заборонені."; + +/* No comment provided by engineer. */ +"Voice messages prohibited!" = "Голосові повідомлення заборонені!"; + +/* No comment provided by engineer. */ +"waiting for answer…" = "в очікуванні відповіді…"; + +/* No comment provided by engineer. */ +"waiting for confirmation…" = "чекаємо на підтвердження…"; + +/* No comment provided by engineer. */ +"Waiting for file" = "Очікування файлу"; + +/* No comment provided by engineer. */ +"Waiting for image" = "Очікування зображення"; + +/* No comment provided by engineer. */ +"Waiting for video" = "Чекаємо на відео"; + +/* No comment provided by engineer. */ +"wants to connect to you!" = "хоче зв'язатися з вами!"; + +/* No comment provided by engineer. */ +"Warning: you may lose some data!" = "Попередження: ви можете втратити деякі дані!"; + +/* No comment provided by engineer. */ +"WebRTC ICE servers" = "Сервери WebRTC ICE"; + +/* time unit */ +"weeks" = "тижнів"; + +/* No comment provided by engineer. */ +"Welcome %@!" = "Ласкаво просимо %@!"; + +/* No comment provided by engineer. */ +"Welcome message" = "Вітальне повідомлення"; + +/* No comment provided by engineer. */ +"What's new" = "Що нового"; + +/* No comment provided by engineer. */ +"When available" = "За наявності"; + +/* No comment provided by engineer. */ +"When people request to connect, you can accept or reject it." = "Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його."; + +/* No comment provided by engineer. */ +"When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують."; + +/* No comment provided by engineer. */ +"With optional welcome message." = "З необов'язковим вітальним повідомленням."; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильний пароль до бази даних"; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Неправильний пароль!"; + +/* No comment provided by engineer. */ +"XFTP servers" = "Сервери XFTP"; + +/* pref value */ +"yes" = "так"; + +/* No comment provided by engineer. */ +"You" = "Ти"; + +/* No comment provided by engineer. */ +"You accepted connection" = "Ви прийняли підключення"; + +/* No comment provided by engineer. */ +"You allow" = "Ви дозволяєте"; + +/* No comment provided by engineer. */ +"You already have a chat profile with the same display name. Please choose another name." = "Ви вже маєте профіль у чаті з таким самим іменем. Будь ласка, виберіть інше ім'я."; + +/* No comment provided by engineer. */ +"You are already connected to %@." = "Ви вже підключені до %@."; + +/* No comment provided by engineer. */ +"You are connected to the server used to receive messages from this contact." = "Ви підключені до сервера, який використовується для отримання повідомлень від цього контакту."; + +/* No comment provided by engineer. */ +"you are invited to group" = "вас запрошують до групи"; + +/* No comment provided by engineer. */ +"You are invited to group" = "Запрошуємо вас до групи"; + +/* No comment provided by engineer. */ +"you are observer" = "ви спостерігач"; + +/* No comment provided by engineer. */ +"You can accept calls from lock screen, without device and app authentication." = "Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми."; + +/* No comment provided by engineer. */ +"You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button." = "Ви також можете підключитися за посиланням. Якщо воно відкриється в браузері, натисніть кнопку **Відкрити в мобільному додатку**."; + +/* No comment provided by engineer. */ +"You can create it later" = "Ви можете створити його пізніше"; + +/* No comment provided by engineer. */ +"You can enable later via Settings" = "Ви можете увімкнути пізніше в Налаштуваннях"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми."; + +/* No comment provided by engineer. */ +"You can hide or mute a user profile - swipe it to the right." = "Ви можете приховати або вимкнути звук профілю користувача - проведіть по ньому вправо."; + +/* notification body */ +"You can now send messages to %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; + +/* No comment provided by engineer. */ +"You can set lock screen notification preview via settings." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; + +/* No comment provided by engineer. */ +"You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Ви можете поділитися посиланням або QR-кодом - будь-хто зможе приєднатися до групи. Ви не втратите учасників групи, якщо згодом видалите її."; + +/* No comment provided by engineer. */ +"You can share this address with your contacts to let them connect with **%@**." = "Ви можете поділитися цією адресою зі своїми контактами, щоб вони могли зв'язатися з **%@**."; + +/* No comment provided by engineer. */ +"You can share your address as a link or QR code - anybody can connect to you." = "Ви можете поділитися своєю адресою у вигляді посилання або QR-коду - будь-хто зможе зв'язатися з вами."; + +/* No comment provided by engineer. */ +"You can start chat via app Settings / Database or by restarting the app" = "Запустити чат можна через Налаштування програми / База даних або перезапустивши програму"; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "Увімкнути SimpleX Lock можна в Налаштуваннях."; + +/* No comment provided by engineer. */ +"You can use markdown to format messages:" = "Ви можете використовувати розмітку для форматування повідомлень:"; + +/* No comment provided by engineer. */ +"You can't send messages!" = "Ви не можете надсилати повідомлення!"; + +/* chat item text */ +"you changed address" = "ви змінили адресу"; + +/* chat item text */ +"you changed address for %@" = "ви змінили адресу на %@"; + +/* snd group event chat item */ +"you changed role for yourself to %@" = "ви змінили роль для себе на %@"; + +/* snd group event chat item */ +"you changed role of %@ to %@" = "ви змінили роль %1$@ на %2$@"; + +/* No comment provided by engineer. */ +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ви контролюєте, через який(і) сервер(и) **отримувати** повідомлення, ваші контакти - сервери, які ви використовуєте для надсилання їм повідомлень."; + +/* No comment provided by engineer. */ +"You could not be verified; please try again." = "Вас не вдалося верифікувати, спробуйте ще раз."; + +/* No comment provided by engineer. */ +"You have no chats" = "У вас немає чатів"; + +/* No comment provided by engineer. */ +"You have to enter passphrase every time the app starts - it is not stored on the device." = "Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої."; + +/* No comment provided by engineer. */ +"You invited a contact" = "Ви запросили контакт"; + +/* No comment provided by engineer. */ +"You joined this group" = "Ви приєдналися до цієї групи"; + +/* No comment provided by engineer. */ +"You joined this group. Connecting to inviting group member." = "Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи."; + +/* snd group event chat item */ +"you left" = "ти пішов"; + +/* No comment provided by engineer. */ +"You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів."; + +/* No comment provided by engineer. */ +"You need to allow your contact to send voice messages to be able to send them." = "Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх."; + +/* No comment provided by engineer. */ +"You rejected group invitation" = "Ви відхилили запрошення до групи"; + +/* snd group event chat item */ +"you removed %@" = "ви видалили %@"; + +/* No comment provided by engineer. */ +"You sent group invitation" = "Ви надіслали запрошення до групи"; + +/* chat list item description */ +"you shared one-time link" = "ви поділилися одноразовим посиланням"; + +/* chat list item description */ +"you shared one-time link incognito" = "ви поділилися одноразовим посиланням інкогніто"; + +/* No comment provided by engineer. */ +"You will be connected to group when the group host's device is online, please wait or check later!" = "Ви будете підключені до групи, коли пристрій господаря групи буде в мережі, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"You will be connected when your connection request is accepted, please wait or check later!" = "Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"You will be connected when your contact's device is online, please wait or check later!" = "Ви будете з'єднані, коли пристрій вашого контакту буде онлайн, будь ласка, зачекайте або перевірте пізніше!"; + +/* No comment provided by engineer. */ +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Вам потрібно буде пройти автентифікацію при запуску або відновленні програми після 30 секунд роботи у фоновому режимі."; + +/* No comment provided by engineer. */ +"You will join a group this link refers to and connect to its group members." = "Ви приєднаєтеся до групи, на яку посилається це посилання, і з'єднаєтеся з її учасниками."; + +/* No comment provided by engineer. */ +"You will still receive calls and notifications from muted profiles when they are active." = "Ви все одно отримуватимете дзвінки та сповіщення від вимкнених профілів, якщо вони активні."; + +/* No comment provided by engineer. */ +"You will stop receiving messages from this group. Chat history will be preserved." = "Ви перестанете отримувати повідомлення від цієї групи. Історія чату буде збережена."; + +/* No comment provided by engineer. */ +"You won't lose your contacts if you later delete your address." = "Ви не втратите свої контакти, якщо згодом видалите свою адресу."; + +/* No comment provided by engineer. */ +"you: " = "ти: "; + +/* No comment provided by engineer. */ +"You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile" = "Ви намагаєтеся запросити контакт, з яким ви поділилися профілем інкогніто, до групи, в якій ви використовуєте свій основний профіль"; + +/* No comment provided by engineer. */ +"You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed" = "Ви використовуєте профіль інкогніто для цієї групи - щоб запобігти поширенню вашого основного профілю, запрошення контактів заборонено"; + +/* No comment provided by engineer. */ +"Your %@ servers" = "Ваші сервери %@"; + +/* No comment provided by engineer. */ +"Your calls" = "Твої дзвінки"; + +/* No comment provided by engineer. */ +"Your chat database" = "Ваша база даних чату"; + +/* No comment provided by engineer. */ +"Your chat database is not encrypted - set passphrase to encrypt it." = "Ваша база даних чату не зашифрована - встановіть ключову фразу, щоб зашифрувати її."; + +/* No comment provided by engineer. */ +"Your chat profile will be sent to group members" = "Ваш профіль у чаті буде надіслано учасникам групи"; + +/* No comment provided by engineer. */ +"Your chat profiles" = "Ваші профілі чату"; + +/* No comment provided by engineer. */ +"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Для завершення з'єднання ваш контакт має бути онлайн.\nВи можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням)."; + +/* No comment provided by engineer. */ +"Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; + +/* No comment provided by engineer. */ +"Your contacts can allow full message deletion." = "Ваші контакти можуть дозволити повне видалення повідомлень."; + +/* No comment provided by engineer. */ +"Your contacts in SimpleX will see it.\nYou can change it in Settings." = "Ваші контакти в SimpleX побачать це.\nВи можете змінити його в Налаштуваннях."; + +/* No comment provided by engineer. */ +"Your contacts will remain connected." = "Ваші контакти залишаться на зв'язку."; + +/* No comment provided by engineer. */ +"Your current chat database will be DELETED and REPLACED with the imported one." = "Ваша поточна база даних чату буде ВИДАЛЕНА і ЗАМІНЕНА імпортованою."; + +/* No comment provided by engineer. */ +"Your current profile" = "Ваш поточний профіль"; + +/* No comment provided by engineer. */ +"Your ICE servers" = "Ваші сервери ICE"; + +/* No comment provided by engineer. */ +"Your preferences" = "Ваші уподобання"; + +/* No comment provided by engineer. */ +"Your privacy" = "Ваша конфіденційність"; + +/* No comment provided by engineer. */ +"Your profile **%@** will be shared." = "Ваш профіль **%@** буде опублікований."; + +/* No comment provided by engineer. */ +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Ваш профіль зберігається на вашому пристрої і доступний лише вашим контактам.\nСервери SimpleX не бачать ваш профіль."; + +/* No comment provided by engineer. */ +"Your profile, contacts and delivered messages are stored on your device." = "Ваш профіль, контакти та доставлені повідомлення зберігаються на вашому пристрої."; + +/* No comment provided by engineer. */ +"Your random profile" = "Ваш випадковий профіль"; + +/* No comment provided by engineer. */ +"Your server" = "Ваш сервер"; + +/* No comment provided by engineer. */ +"Your server address" = "Адреса вашого сервера"; + +/* No comment provided by engineer. */ +"Your settings" = "Ваші налаштування"; + +/* No comment provided by engineer. */ +"Your SimpleX address" = "Ваша адреса SimpleX"; + +/* No comment provided by engineer. */ +"Your SMP servers" = "Ваші SMP-сервери"; + +/* No comment provided by engineer. */ +"Your XFTP servers" = "Ваші XFTP-сервери"; + diff --git a/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings new file mode 100644 index 0000000000..2e3c6b8930 --- /dev/null +++ b/apps/ios/uk.lproj/SimpleX--iOS--InfoPlist.strings @@ -0,0 +1,15 @@ +/* Bundle name */ +"CFBundleName" = "SimpleX"; + +/* Privacy - Camera Usage Description */ +"NSCameraUsageDescription" = "SimpleX потребує доступу до камери, щоб сканувати QR-коди для з'єднання з іншими користувачами та для відеодзвінків."; + +/* Privacy - Face ID Usage Description */ +"NSFaceIDUsageDescription" = "SimpleX використовує Face ID для локальної автентифікації"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "SimpleX потребує доступу до мікрофона для аудіо та відео дзвінків, а також для запису голосових повідомлень."; + +/* Privacy - Photo Library Additions Usage Description */ +"NSPhotoLibraryAddUsageDescription" = "SimpleX потребує доступу до фототеки для збереження захоплених та отриманих медіафайлів"; + diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index f3db4f4593..25eadf44d4 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -19,6 +19,9 @@ /* No comment provided by engineer. */ "_italic_" = "\\_斜体_"; +/* No comment provided by engineer. */ +"- more stable message delivery.\n- a bit better groups.\n- and more!" = "- 更稳定的传输!\n- 更好的社群!\n- 以及更多!"; + /* No comment provided by engineer. */ "- voice messages up to 5 minutes.\n- custom time to disappear.\n- editing history." = "- 语音消息最长5分钟。\n- 自定义限时消息。\n- 编辑消息历史。"; @@ -85,6 +88,15 @@ /* No comment provided by engineer. */ "*bold*" = "\\*加粗*"; +/* copied message info title, # <title> */ +"# %@" = "# %@"; + +/* copied message info */ +"## History" = "## 历史"; + +/* copied message info */ +"## In reply to" = "## 回复"; + /* No comment provided by engineer. */ "#secret#" = "#秘密#"; @@ -103,6 +115,12 @@ /* No comment provided by engineer. */ "%@ %@" = "%@ %@"; +/* No comment provided by engineer. */ +"%@ and %@ connected" = "%@ 和%@ 以建立连接"; + +/* copied message info, <sender> at <time> */ +"%@ at %@:" = "@ %2$@:"; + /* notification title */ "%@ is connected!" = "%@ 已连接!"; @@ -118,6 +136,9 @@ /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; +/* No comment provided by engineer. */ +"%@, %@ and %lld other members connected" = "%@, %@ 和 %lld 个成员"; + /* copied message info */ "%@:" = "%@:"; @@ -160,6 +181,9 @@ /* No comment provided by engineer. */ "%lld minutes" = "%lld 分钟"; +/* No comment provided by engineer. */ +"%lld new interface languages" = "%lld 种新的界面语言"; + /* No comment provided by engineer. */ "%lld second(s)" = "%lld 秒"; @@ -232,14 +256,14 @@ /* No comment provided by engineer. */ "30 seconds" = "30秒"; +/* No comment provided by engineer. */ +"A few more things" = "一些杂项"; + /* notification title */ "A new contact" = "新联系人"; /* No comment provided by engineer. */ -"A random profile will be sent to the contact that you received this link from" = "一个随机个人资料将被发送至给予您链接的联系人那里"; - -/* No comment provided by engineer. */ -"A random profile will be sent to your contact" = "一个随机资料将发送给您的联系人"; +"A new random profile will be shared." = "创建一个随机的共享文件。"; /* No comment provided by engineer. */ "A separate TCP connection will be used **for each chat profile you have in the app**." = "一个单独的 TCP 连接将被用于**您在应用程序中的每个聊天资料**。"; @@ -276,12 +300,12 @@ "Accept" = "接受"; /* No comment provided by engineer. */ -"Accept contact" = "接受联系人"; +"Accept connection request?" = "接受联系人?"; /* notification body */ "Accept contact request from %@?" = "接受来自 %@ 的联系人请求?"; -/* No comment provided by engineer. */ +/* accept contact request via notification */ "Accept incognito" = "接受隐身聊天"; /* call status */ @@ -323,6 +347,12 @@ /* No comment provided by engineer. */ "Advanced network settings" = "高级网络设置"; +/* chat item text */ +"agreeing encryption for %@…" = "正在协商将加密应用于 %@…"; + +/* chat item text */ +"agreeing encryption…" = "同意加密…"; + /* No comment provided by engineer. */ "All app data is deleted." = "已删除所有应用程序数据。"; @@ -588,6 +618,12 @@ /* rcv group event chat item */ "changed your role to %@" = "更改您的角色为 %@"; +/* chat item text */ +"changing address for %@…" = "正在将变更的地址应用于 %@…"; + +/* chat item text */ +"changing address…" = "更改地址…"; + /* No comment provided by engineer. */ "Chat archive" = "聊天档案"; @@ -675,11 +711,17 @@ /* server test step */ "Connect" = "连接"; +/* No comment provided by engineer. */ +"Connect directly" = "直接连接"; + +/* No comment provided by engineer. */ +"Connect incognito" = "在隐身状态下连接"; + /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "连接到 SimpleX Chat 开发者。"; /* No comment provided by engineer. */ -"Connect via contact link?" = "通过联系人链接进行连接?"; +"Connect via contact link" = "通过联系人链接进行连接"; /* No comment provided by engineer. */ "Connect via group link?" = "通过群组链接连接?"; @@ -691,7 +733,7 @@ "Connect via link / QR code" = "通过群组链接/二维码连接"; /* No comment provided by engineer. */ -"Connect via one-time link?" = "通过一次性链接连接?"; +"Connect via one-time link" = "通过一次性链接连接"; /* No comment provided by engineer. */ "connected" = "已连接"; @@ -735,9 +777,6 @@ /* chat list item title (it should not be shown */ "connection established" = "连接已建立"; -/* No comment provided by engineer. */ -"Connection request" = "连接请求"; - /* No comment provided by engineer. */ "Connection request sent!" = "已发送连接请求!"; @@ -777,6 +816,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "联系人偏好设置"; +/* No comment provided by engineer. */ +"Contacts" = "联系人"; + /* No comment provided by engineer. */ "Contacts can mark messages for deletion; you will be able to view them." = "联系人可以将信息标记为删除;您将可以查看这些信息。"; @@ -912,6 +954,12 @@ /* pref value */ "default (%@)" = "默认 (%@)"; +/* No comment provided by engineer. */ +"default (no)" = "默认(否)"; + +/* No comment provided by engineer. */ +"default (yes)" = "默认 (是)"; + /* chat item action */ "Delete" = "删除"; @@ -1029,6 +1077,15 @@ /* rcv group event chat item */ "deleted group" = "已删除群组"; +/* No comment provided by engineer. */ +"Delivery" = "传送"; + +/* No comment provided by engineer. */ +"Delivery receipts are disabled!" = "送达回执已禁用!"; + +/* No comment provided by engineer. */ +"Delivery receipts!" = "送达回执!"; + /* No comment provided by engineer. */ "Description" = "描述"; @@ -1062,9 +1119,18 @@ /* No comment provided by engineer. */ "Direct messages between members are prohibited in this group." = "此群中禁止成员之间私信。"; +/* No comment provided by engineer. */ +"Disable (keep overrides)" = "禁用(保留覆盖)"; + +/* No comment provided by engineer. */ +"Disable for all" = "全部禁用"; + /* authentication reason */ "Disable SimpleX Lock" = "禁用 SimpleX 锁定"; +/* No comment provided by engineer. */ +"disabled" = "关闭"; + /* No comment provided by engineer. */ "Disappearing message" = "限时消息"; @@ -1101,6 +1167,9 @@ /* No comment provided by engineer. */ "Don't create address" = "不创建地址"; +/* No comment provided by engineer. */ +"Don't enable" = "不要启用"; + /* No comment provided by engineer. */ "Don't show again" = "不再显示"; @@ -1131,9 +1200,15 @@ /* No comment provided by engineer. */ "Enable" = "启用"; +/* No comment provided by engineer. */ +"Enable (keep overrides)" = "启用(保持覆盖)"; + /* No comment provided by engineer. */ "Enable automatic message deletion?" = "启用自动删除消息?"; +/* No comment provided by engineer. */ +"Enable for all" = "全部启用"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "启用即时通知?"; @@ -1173,6 +1248,9 @@ /* No comment provided by engineer. */ "Encrypt database?" = "加密数据库?"; +/* No comment provided by engineer. */ +"Encrypt local files" = "加密本地文件"; + /* No comment provided by engineer. */ "Encrypted database" = "加密数据库"; @@ -1194,6 +1272,30 @@ /* notification */ "Encrypted message: unexpected error" = "加密消息:意外错误"; +/* chat item text */ +"encryption agreed" = "已同意加密"; + +/* chat item text */ +"encryption agreed for %@" = "同意对 %@ 进行加密"; + +/* chat item text */ +"encryption ok" = "可以加密"; + +/* chat item text */ +"encryption ok for %@" = "对 %@ 进行加密"; + +/* chat item text */ +"encryption re-negotiation allowed" = "允许重新进行加密协商"; + +/* chat item text */ +"encryption re-negotiation allowed for %@" = "允许对 %@ 进行加密重新协商"; + +/* chat item text */ +"encryption re-negotiation required" = "需要重新进行加密协商"; + +/* chat item text */ +"encryption re-negotiation required for %@" = "需要为 %@ 重新进行加密协商"; + /* No comment provided by engineer. */ "ended" = "已结束"; @@ -1260,6 +1362,9 @@ /* No comment provided by engineer. */ "Error creating profile!" = "创建资料错误!"; +/* No comment provided by engineer. */ +"Error decrypting file" = "解密文件时出错"; + /* No comment provided by engineer. */ "Error deleting chat database" = "删除聊天数据库错误"; @@ -1284,6 +1389,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "删除用户资料错误"; +/* No comment provided by engineer. */ +"Error enabling delivery receipts!" = "启用送达回执出错!"; + /* No comment provided by engineer. */ "Error enabling notifications" = "启用通知错误"; @@ -1332,6 +1440,9 @@ /* No comment provided by engineer. */ "Error sending message" = "发送消息错误"; +/* No comment provided by engineer. */ +"Error setting delivery receipts!" = "设置送达回执出错!"; + /* No comment provided by engineer. */ "Error starting chat" = "启动聊天错误"; @@ -1341,6 +1452,9 @@ /* No comment provided by engineer. */ "Error switching profile!" = "切换资料错误!"; +/* No comment provided by engineer. */ +"Error synchronizing connection" = "同步连接错误"; + /* No comment provided by engineer. */ "Error updating group link" = "更新群组链接错误"; @@ -1365,6 +1479,12 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "错误:URL 无效"; +/* No comment provided by engineer. */ +"Even when disabled in the conversation." = "即使在对话中被禁用。"; + +/* No comment provided by engineer. */ +"event happened" = "发生的事"; + /* No comment provided by engineer. */ "Exit without saving" = "退出而不保存"; @@ -1413,9 +1533,33 @@ /* No comment provided by engineer. */ "Files and media prohibited!" = "禁止文件和媒体!"; +/* No comment provided by engineer. */ +"Filter unread and favorite chats." = "过滤未读和收藏的聊天记录。"; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "终于我们有它们了! 🚀"; +/* No comment provided by engineer. */ +"Find chats faster" = "更快地查找聊天记录"; + +/* No comment provided by engineer. */ +"Fix" = "修复"; + +/* No comment provided by engineer. */ +"Fix connection" = "修复连接"; + +/* No comment provided by engineer. */ +"Fix connection?" = "修复连接?"; + +/* No comment provided by engineer. */ +"Fix encryption after restoring backups." = "修复还原备份后的加密问题。"; + +/* No comment provided by engineer. */ +"Fix not supported by contact" = "修复联系人不支持的问题"; + +/* No comment provided by engineer. */ +"Fix not supported by group member" = "修复群组成员不支持的问题"; + /* No comment provided by engineer. */ "For console" = "用于控制台"; @@ -1539,7 +1683,7 @@ /* No comment provided by engineer. */ "Hide:" = "隐藏:"; -/* copied message info */ +/* No comment provided by engineer. */ "History" = "历史记录"; /* time unit */ @@ -1608,6 +1752,9 @@ /* No comment provided by engineer. */ "Improved server configuration" = "改进的服务器配置"; +/* No comment provided by engineer. */ +"In reply to" = "答复"; + /* No comment provided by engineer. */ "Incognito" = "隐身聊天"; @@ -1615,10 +1762,7 @@ "Incognito mode" = "隐身模式"; /* No comment provided by engineer. */ -"Incognito mode is not supported here - your main profile will be sent to group members" = "此处不支持隐身模式——您的主要个人资料将发送给群组成员"; - -/* No comment provided by engineer. */ -"Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created." = "隐身模式可以保护你的主要个人资料名称和图像的隐私——对于每个新的联系人,都会创建一个新的随机个人资料。"; +"Incognito mode protects your privacy by using a new random profile for each contact." = "隐身模式会为每个联系人使用一个新的随机配置文件,从而保护你的隐私。"; /* chat list item description */ "incognito via contact address link" = "通过联系人地址链接隐身聊天"; @@ -1683,6 +1827,9 @@ /* No comment provided by engineer. */ "Invalid server address!" = "无效的服务器地址!"; +/* item status text */ +"Invalid status" = "无效状态"; + /* No comment provided by engineer. */ "Invitation expired!" = "邀请已过期!"; @@ -1761,6 +1908,9 @@ /* No comment provided by engineer. */ "Joining group" = "加入群组中"; +/* No comment provided by engineer. */ +"Keep your connections" = "保持连接"; + /* No comment provided by engineer. */ "Keychain error" = "钥匙串错误"; @@ -1818,6 +1968,9 @@ /* No comment provided by engineer. */ "Make a private connection" = "建立私密连接"; +/* No comment provided by engineer. */ +"Make one message disappear" = "使一条消息消失"; + /* No comment provided by engineer. */ "Make profile private!" = "将个人资料设为私密!"; @@ -1866,9 +2019,12 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "成员将被移出群组——此操作无法撤消!"; -/* No comment provided by engineer. */ +/* item status text */ "Message delivery error" = "消息传递错误"; +/* No comment provided by engineer. */ +"Message delivery receipts!" = "消息送达回执!"; + /* No comment provided by engineer. */ "Message draft" = "消息草稿"; @@ -1935,6 +2091,9 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "更多改进即将推出!"; +/* item status description */ +"Most likely this connection is deleted." = "此连接很可能已被删除。"; + /* No comment provided by engineer. */ "Most likely this contact has deleted the connection with you." = "很可能此联系人已经删除了与您的联系。"; @@ -2007,6 +2166,9 @@ /* No comment provided by engineer. */ "No contacts to add" = "没有联系人可添加"; +/* No comment provided by engineer. */ +"No delivery information" = "无送达信息"; + /* No comment provided by engineer. */ "No device token!" = "无设备令牌!"; @@ -2019,6 +2181,9 @@ /* No comment provided by engineer. */ "No group!" = "未找到群组!"; +/* No comment provided by engineer. */ +"No history" = "无历史记录"; + /* No comment provided by engineer. */ "No permission to record voice message" = "没有录制语音消息的权限"; @@ -2176,8 +2341,8 @@ /* No comment provided by engineer. */ "Paste received link" = "粘贴收到的链接"; -/* No comment provided by engineer. */ -"Paste the link you received into the box below to connect with your contact." = "将您收到的链接粘贴到下面的框中以与您的联系人联系。"; +/* placeholder */ +"Paste the link you received to connect with your contact." = "将您收到的链接粘贴到下面的框中以与您的联系人联系。"; /* No comment provided by engineer. */ "peer-to-peer" = "点对点"; @@ -2305,12 +2470,18 @@ /* No comment provided by engineer. */ "Protocol timeout" = "协议超时"; +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "每 KB 协议超时"; + /* No comment provided by engineer. */ "Push notifications" = "推送通知"; /* No comment provided by engineer. */ "Rate the app" = "评价此应用程序"; +/* chat item menu */ +"React…" = "回应…"; + /* No comment provided by engineer. */ "Read" = "已读"; @@ -2329,6 +2500,9 @@ /* No comment provided by engineer. */ "Read more in our GitHub repository." = "在我们的 GitHub 仓库中阅读更多内容。"; +/* No comment provided by engineer. */ +"Receipts are disabled" = "回执已禁用"; + /* No comment provided by engineer. */ "received answer…" = "已收到回复……"; @@ -2359,6 +2533,12 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "对方会在您键入时看到更新。"; +/* No comment provided by engineer. */ +"Reconnect all connected servers to force message delivery. It uses additional traffic." = "重新连接所有已连接的服务器以强制发送信息。这会耗费更多流量。"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "是否重新连接服务器?"; + /* No comment provided by engineer. */ "Record updated at" = "记录更新于"; @@ -2372,7 +2552,7 @@ "Reject" = "拒绝"; /* No comment provided by engineer. */ -"Reject contact (sender NOT notified)" = "拒绝联系人(发送者不会被通知)"; +"Reject (sender NOT notified)" = "拒绝联系人(发送者不会被通知)"; /* No comment provided by engineer. */ "Reject contact request" = "拒绝联系人请求"; @@ -2407,6 +2587,15 @@ /* rcv group event chat item */ "removed you" = "已将您移除"; +/* No comment provided by engineer. */ +"Renegotiate" = "重新协商"; + +/* No comment provided by engineer. */ +"Renegotiate encryption" = "重新协商加密"; + +/* No comment provided by engineer. */ +"Renegotiate encryption?" = "重新协商加密?"; + /* chat item action */ "Reply" = "回复"; @@ -2545,6 +2734,9 @@ /* No comment provided by engineer. */ "Security code" = "安全码"; +/* chat item text */ +"security code changed" = "安全密码已更改"; + /* No comment provided by engineer. */ "Select" = "选择"; @@ -2566,6 +2758,9 @@ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "发送实时消息——它会在您键入时为收件人更新"; +/* No comment provided by engineer. */ +"Send delivery receipts to" = "将送达回执发送给"; + /* No comment provided by engineer. */ "Send direct message" = "发送私信"; @@ -2587,6 +2782,9 @@ /* No comment provided by engineer. */ "Send questions and ideas" = "发送问题和想法"; +/* No comment provided by engineer. */ +"Send receipts" = "发送回执"; + /* No comment provided by engineer. */ "Send them from gallery or custom keyboards." = "发送它们来自图库或自定义键盘。"; @@ -2596,9 +2794,27 @@ /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "发送人可能已删除连接请求。"; +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts in all visible chat profiles." = "将对所有可见聊天配置文件中的所有联系人启用送达回执功能。"; + +/* No comment provided by engineer. */ +"Sending delivery receipts will be enabled for all contacts." = "将对所有联系人启用送达回执功能。"; + /* No comment provided by engineer. */ "Sending file will be stopped." = "即将停止发送文件。"; +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld contacts" = "已为 %lld 联系人禁用送达回执功能"; + +/* No comment provided by engineer. */ +"Sending receipts is disabled for %lld groups" = "已为 %lld 组禁用送达回执功能"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld contacts" = "已为 %lld 联系人启用送达回执功能"; + +/* No comment provided by engineer. */ +"Sending receipts is enabled for %lld groups" = "已为 %lld 组启用送达回执功能"; + /* No comment provided by engineer. */ "Sending via" = "发送通过"; @@ -2683,6 +2899,9 @@ /* No comment provided by engineer. */ "Show developer options" = "显示开发者选项"; +/* No comment provided by engineer. */ +"Show last messages" = "显示最近的消息"; + /* No comment provided by engineer. */ "Show preview" = "显示预览"; @@ -2731,6 +2950,9 @@ /* No comment provided by engineer. */ "Skipped messages" = "已跳过消息"; +/* No comment provided by engineer. */ +"Small groups (max 20)" = "小群组(最多 20 人)"; + /* No comment provided by engineer. */ "SMP servers" = "SMP 服务器"; @@ -2866,6 +3088,9 @@ /* No comment provided by engineer. */ "The created archive is available via app Settings / Database / Old database archive." = "创建的归档文件可以通过应用设置/数据库/旧数据库归档访问。"; +/* No comment provided by engineer. */ +"The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "加密正在运行,不需要新的加密协议。这可能会导致连接错误!"; + /* No comment provided by engineer. */ "The group is fully decentralized – it is visible only to the members." = "该小组是完全分散式的——它只对成员可见。"; @@ -2890,6 +3115,9 @@ /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "该资料仅与您的联系人共享。"; +/* No comment provided by engineer. */ +"The second tick we missed! ✅" = "我们错过的第二个\"√\"!✅"; + /* No comment provided by engineer. */ "The sender will NOT be notified" = "发送者将不会收到通知"; @@ -2905,6 +3133,12 @@ /* No comment provided by engineer. */ "There should be at least one visible user profile." = "应该至少有一个可见的用户资料。"; +/* No comment provided by engineer. */ +"These settings are for your current profile **%@**." = "这些设置适用于您当前的配置文件 **%@**。"; + +/* No comment provided by engineer. */ +"They can be overridden in contact and group settings." = "可以在联系人和群组设置中覆盖它们。"; + /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "此操作无法撤消——所有接收和发送的文件和媒体都将被删除。 低分辨率图片将保留。"; @@ -2917,6 +3151,9 @@ /* notification title */ "this contact" = "这个联系人"; +/* No comment provided by engineer. */ +"This group has over %lld members, delivery receipts are not sent." = "该组有超过 %lld 个成员,不发送送货单。"; + /* No comment provided by engineer. */ "This group no longer exists." = "该群组已不存在。"; @@ -2929,9 +3166,6 @@ /* No comment provided by engineer. */ "To connect, your contact can scan QR code or use the link in the app." = "您的联系人可以扫描二维码或使用应用程序中的链接来建立连接。"; -/* No comment provided by engineer. */ -"To find the profile used for an incognito connection, tap the contact or group name on top of the chat." = "要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。"; - /* No comment provided by engineer. */ "To make a new connection" = "建立新连接"; @@ -2977,7 +3211,7 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "无法录制语音消息"; -/* No comment provided by engineer. */ +/* item status description */ "Unexpected error: %@" = "意外错误: %@"; /* No comment provided by engineer. */ @@ -3064,12 +3298,18 @@ /* No comment provided by engineer. */ "Use chat" = "使用聊天"; +/* No comment provided by engineer. */ +"Use current profile" = "使用当前配置文件"; + /* No comment provided by engineer. */ "Use for new connections" = "用于新连接"; /* No comment provided by engineer. */ "Use iOS call interface" = "使用 iOS 通话界面"; +/* No comment provided by engineer. */ +"Use new incognito profile" = "使用新的隐身配置文件"; + /* No comment provided by engineer. */ "Use server" = "使用服务器"; @@ -3238,6 +3478,12 @@ /* No comment provided by engineer. */ "You can create it later" = "您可以以后创建它"; +/* No comment provided by engineer. */ +"You can enable later via Settings" = "您可以稍后在设置中启用它"; + +/* No comment provided by engineer. */ +"You can enable them later via app Privacy & Security settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。"; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "您可以隐藏或静音用户个人资料——只需向右滑动。"; @@ -3293,7 +3539,7 @@ "You have to enter passphrase every time the app starts - it is not stored on the device." = "您必须在每次应用程序启动时输入密码——它不存储在设备上。"; /* No comment provided by engineer. */ -"You invited your contact" = "您邀请了您的联系人"; +"You invited a contact" = "您邀请了您的联系人"; /* No comment provided by engineer. */ "You joined this group" = "您已加入此群组"; @@ -3373,9 +3619,6 @@ /* No comment provided by engineer. */ "Your chat profile will be sent to group members" = "您的聊天资料将被发送给群组成员"; -/* No comment provided by engineer. */ -"Your chat profile will be sent to your contact" = "您的聊天资料将被发送给您的联系人"; - /* No comment provided by engineer. */ "Your chat profiles" = "您的聊天资料"; @@ -3410,10 +3653,10 @@ "Your privacy" = "您的隐私设置"; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。\nSimpleX 服务器无法看到您的资料。"; +"Your profile **%@** will be shared." = "您的个人资料 **%@** 将被共享。"; /* No comment provided by engineer. */ -"Your profile will be sent to the contact that you received this link from" = "您的个人资料将发送给您收到此链接的联系人"; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "您的资料存储在您的设备上并仅与您的联系人共享。\nSimpleX 服务器无法看到您的资料。"; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "您的资料、联系人和发送的消息存储在您的设备上。"; diff --git a/apps/multiplatform/.gitignore b/apps/multiplatform/.gitignore index d8419431db..81d296183d 100644 --- a/apps/multiplatform/.gitignore +++ b/apps/multiplatform/.gitignore @@ -11,7 +11,7 @@ local.properties common/src/commonMain/cpp/android/libs/ common/src/commonMain/cpp/desktop/libs/ -common/src/commonMain/resources/libs/ +desktop/src/jvmMain/resources/libs/ android/build android/release common/build diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 62dab0b953..67a8fea87b 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -7,17 +7,13 @@ plugins { id("org.jetbrains.kotlin.plugin.serialization") } -repositories { - maven("https://jitpack.io") -} - android { compileSdkVersion(33) defaultConfig { applicationId = "chat.simplex.app" minSdkVersion(26) - targetSdkVersion(32) + targetSdkVersion(33) // !!! // skip version code after release to F-Droid, as it uses two version codes versionCode = (extra["android.version_code"] as String).toInt() @@ -87,16 +83,22 @@ android { // Comma separated list of languages that will be included in the apk android.defaultConfig.resConfigs( "en", + "ar", + "bg", "cs", "de", "es", + "fi", "fr", "it", + "iw", "ja", "nl", "pl", "pt-rBR", "ru", + "th", + "uk", "zh-rCN" ) // } @@ -124,14 +126,19 @@ dependencies { //implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}") //implementation("androidx.compose.material:material:$compose_version") //implementation("androidx.compose.ui:ui-tooling-preview:$compose_version") + implementation("androidx.appcompat:appcompat:1.5.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.4.1") implementation("androidx.lifecycle:lifecycle-process:2.4.1") implementation("androidx.activity:activity-compose:1.5.0") + val work_version = "2.7.1" + implementation("androidx.work:work-runtime-ktx:$work_version") + implementation("androidx.work:work-multiprocess:$work_version") + + implementation("com.jakewharton:process-phoenix:2.1.2") + //implementation("androidx.compose.material:material-icons-extended:$compose_version") //implementation("androidx.compose.ui:ui-util:$compose_version") - implementation("com.google.accompanist:accompanist-pager:0.25.1") - testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 090cb3b217..512e9efc10 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,84 +1,40 @@ package chat.simplex.app -import android.app.Application import android.content.Intent import android.net.Uri import android.os.* -import android.os.SystemClock.elapsedRealtime -import android.util.Log import android.view.WindowManager import androidx.activity.compose.setContent -import androidx.activity.viewModels -import androidx.compose.animation.core.* -import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.* -import chat.simplex.app.MainActivity.Companion.enteredBackground -import chat.simplex.app.model.* +import chat.simplex.app.model.NtfManager import chat.simplex.app.model.NtfManager.getUserIdFromIntent -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.SplashView -import chat.simplex.app.views.call.ActiveCallView -import chat.simplex.app.views.call.IncomingCallAlertView -import chat.simplex.app.views.chat.ChatView -import chat.simplex.app.views.chatlist.* -import chat.simplex.app.views.database.DatabaseErrorView -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword -import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword -import chat.simplex.app.views.localauth.SetAppPasscodeView -import chat.simplex.app.views.newchat.* -import chat.simplex.app.views.onboarding.* -import chat.simplex.app.views.usersettings.LAMode -import chat.simplex.app.views.usersettings.SetDeliveryReceiptsView +import chat.simplex.common.* +import chat.simplex.common.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.* +import chat.simplex.common.platform.* import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* -import kotlinx.coroutines.flow.distinctUntilChanged import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { - companion object { - /** - * We don't want these values to be bound to Activity lifecycle since activities are changed often, for example, when a user - * clicks on new message in notification. In this case savedInstanceState will be null (this prevents restoring the values) - * See [SimplexService.onTaskRemoved] for another part of the logic which nullifies the values when app closed by the user - * */ - val userAuthorized = mutableStateOf<Boolean?>(null) - val enteredBackground = mutableStateOf<Long?>(null) - // Remember result and show it after orientation change - private val laFailed = mutableStateOf(false) - - fun clearAuthState() { - userAuthorized.value = null - enteredBackground.value = null - } - } - private val vm by viewModels<SimplexViewModel>() - private val destroyedAfterBackPress = mutableStateOf(false) override fun onCreate(savedInstanceState: Bundle?) { applyAppLocale(ChatModel.controller.appPrefs.appLanguage) super.onCreate(savedInstanceState) - SimplexApp.context.mainActivity = WeakReference(this) // testJson() - val m = vm.chatModel + mainActivity = WeakReference(this) // When call ended and orientation changes, it re-process old intent, it's unneeded. // Only needed to be processed on first creation of activity if (savedInstanceState == null) { - processNotificationIntent(intent, m) - processIntent(intent, m) - processExternalIntent(intent, m) + processNotificationIntent(intent) + processIntent(intent) + processExternalIntent(intent) } - if (m.controller.appPrefs.privacyProtectScreen.get()) { + if (ChatController.appPrefs.privacyProtectScreen.get()) { Log.d(TAG, "onCreate: set FLAG_SECURE") window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, @@ -87,17 +43,7 @@ class MainActivity: FragmentActivity() { } setContent { SimpleXTheme { - Surface(color = MaterialTheme.colors.background) { - MainPage( - m, - userAuthorized, - laFailed, - destroyedAfterBackPress, - ::runAuthenticate, - ::setPerformLA, - showLANotice = { showLANotice(m.controller.appPrefs.laNoticeShown) } - ) - } + AppScreen() } } SimplexApp.context.schedulePeriodicServiceRestartWorker() @@ -106,38 +52,29 @@ class MainActivity: FragmentActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - processIntent(intent, vm.chatModel) - processExternalIntent(intent, vm.chatModel) + processIntent(intent) + processExternalIntent(intent) } override fun onResume() { super.onResume() - val enteredBackgroundVal = enteredBackground.value - val delay = vm.chatModel.controller.appPrefs.laLockDelay.get() - if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) { - if (userAuthorized.value != false) { - /** [runAuthenticate] will be called in [MainPage] if needed. Making like this prevents double showing of passcode on start */ - setAuthState() - } else if (!vm.chatModel.activeCallViewIsVisible.value) { - runAuthenticate() - } - } + AppLock.recheckAuthState() } override fun onPause() { super.onPause() /** - * When new activity is created after a click on notification, the old one receives onPause before - * recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent - * unwanted multiple auth dialogs from [runAuthenticate] - * */ - enteredBackground.value = elapsedRealtime() + * When new activity is created after a click on notification, the old one receives onPause before + * recreation but receives onStop after recreation. So using both (onPause and onStop) to prevent + * unwanted multiple auth dialogs from [runAuthenticate] + * */ + AppLock.appWasHidden() } override fun onStop() { super.onStop() - VideoPlayer.stopAll() - enteredBackground.value = elapsedRealtime() + VideoPlayerHolder.stopAll() + AppLock.appWasHidden() } override fun onBackPressed() { @@ -150,481 +87,52 @@ class MainActivity: FragmentActivity() { super.onBackPressed() } - if (!onBackPressedDispatcher.hasEnabledCallbacks() && vm.chatModel.controller.appPrefs.performLA.get()) { + if (!onBackPressedDispatcher.hasEnabledCallbacks() && ChatController.appPrefs.performLA.get()) { // When pressed Back and there is no one wants to process the back event, clear auth state to force re-auth on launch - clearAuthState() - laFailed.value = true - destroyedAfterBackPress.value = true + AppLock.clearAuthState() + AppLock.laFailed.value = true + AppLock.destroyedAfterBackPress.value = true } if (!onBackPressedDispatcher.hasEnabledCallbacks()) { // Drop shared content SimplexApp.context.chatModel.sharedContent.value = null } } - - private fun setAuthState() { - userAuthorized.value = !vm.chatModel.controller.appPrefs.performLA.get() - } - - private fun runAuthenticate() { - val m = vm.chatModel - setAuthState() - if (userAuthorized.value == false) { - // To make Main thread free in order to allow to Compose to show blank view that hiding content underneath of it faster on slow devices - CoroutineScope(Dispatchers.Default).launch { - delay(50) - withContext(Dispatchers.Main) { - authenticate( - if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) - generalGetString(MR.strings.auth_unlock) - else - generalGetString(MR.strings.la_enter_app_passcode), - if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) - generalGetString(MR.strings.auth_log_in_using_credential) - else - generalGetString(MR.strings.auth_unlock), - selfDestruct = true, - completed = { laResult -> - when (laResult) { - LAResult.Success -> - userAuthorized.value = true - is LAResult.Failed -> { /* Can be called multiple times on every failure */ } - is LAResult.Error -> { - laFailed.value = true - if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) { - laFailedAlert() - } - } - is LAResult.Unavailable -> { - userAuthorized.value = true - m.performLA.value = false - m.controller.appPrefs.performLA.set(false) - laUnavailableTurningOffAlert() - } - } - } - ) - } - } - } - } - - private fun showLANotice(laNoticeShown: SharedPreference<Boolean>) { - Log.d(TAG, "showLANotice") - if (!laNoticeShown.get()) { - laNoticeShown.set(true) - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.la_notice_title_simplex_lock), - text = generalGetString(MR.strings.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled), - confirmText = generalGetString(MR.strings.la_notice_turn_on), - onConfirm = { - withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager - showChooseLAMode(laNoticeShown) - } - } - ) - } - } - - private fun showChooseLAMode(laNoticeShown: SharedPreference<Boolean>) { - Log.d(TAG, "showLANotice") - laNoticeShown.set(true) - AlertManager.shared.showAlertDialogStacked( - title = generalGetString(MR.strings.la_lock_mode), - text = null, - confirmText = generalGetString(MR.strings.la_lock_mode_passcode), - dismissText = generalGetString(MR.strings.la_lock_mode_system), - onConfirm = { - AlertManager.shared.hideAlert() - setPasscode() - }, - onDismiss = { - AlertManager.shared.hideAlert() - initialEnableLA() - } - ) - } - - private fun initialEnableLA() { - val m = vm.chatModel - val appPrefs = m.controller.appPrefs - m.controller.appPrefs.laMode.set(LAMode.SYSTEM) - authenticate( - generalGetString(MR.strings.auth_enable_simplex_lock), - generalGetString(MR.strings.auth_confirm_credential), - completed = { laResult -> - when (laResult) { - LAResult.Success -> { - m.performLA.value = true - appPrefs.performLA.set(true) - laTurnedOnAlert() - } - is LAResult.Failed -> { /* Can be called multiple times on every failure */ } - is LAResult.Error -> { - m.performLA.value = false - appPrefs.performLA.set(false) - laFailedAlert() - } - is LAResult.Unavailable -> { - m.performLA.value = false - appPrefs.performLA.set(false) - m.showAdvertiseLAUnavailableAlert.value = true - } - } - } - ) - } - - private fun setPasscode() { - val chatModel = vm.chatModel - val appPrefs = chatModel.controller.appPrefs - ModalManager.shared.showCustomModal { close -> - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { - SetAppPasscodeView( - submit = { - chatModel.performLA.value = true - appPrefs.performLA.set(true) - appPrefs.laMode.set(LAMode.PASSCODE) - laTurnedOnAlert() - }, - cancel = { - chatModel.performLA.value = false - appPrefs.performLA.set(false) - laPasscodeNotSetAlert() - }, - close = close - ) - } - } - } - - private fun setPerformLA(on: Boolean) { - vm.chatModel.controller.appPrefs.laNoticeShown.set(true) - if (on) { - enableLA() - } else { - disableLA() - } - } - - private fun enableLA() { - val m = vm.chatModel - authenticate( - if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) - generalGetString(MR.strings.auth_enable_simplex_lock) - else - generalGetString(MR.strings.new_passcode), - if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) - generalGetString(MR.strings.auth_confirm_credential) - else - "", - completed = { laResult -> - val prefPerformLA = m.controller.appPrefs.performLA - when (laResult) { - LAResult.Success -> { - m.performLA.value = true - prefPerformLA.set(true) - laTurnedOnAlert() - } - is LAResult.Failed -> { /* Can be called multiple times on every failure */ } - is LAResult.Error -> { - m.performLA.value = false - prefPerformLA.set(false) - laFailedAlert() - } - is LAResult.Unavailable -> { - m.performLA.value = false - prefPerformLA.set(false) - laUnavailableInstructionAlert() - } - } - } - ) - } - - private fun disableLA() { - val m = vm.chatModel - authenticate( - if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) - generalGetString(MR.strings.auth_disable_simplex_lock) - else - generalGetString(MR.strings.la_enter_app_passcode), - if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) - generalGetString(MR.strings.auth_confirm_credential) - else - generalGetString(MR.strings.auth_disable_simplex_lock), - completed = { laResult -> - val prefPerformLA = m.controller.appPrefs.performLA - val selfDestructPref = m.controller.appPrefs.selfDestruct - when (laResult) { - LAResult.Success -> { - m.performLA.value = false - prefPerformLA.set(false) - ksAppPassword.remove() - selfDestructPref.set(false) - ksSelfDestructPassword.remove() - } - is LAResult.Failed -> { /* Can be called multiple times on every failure */ } - is LAResult.Error -> { - m.performLA.value = true - prefPerformLA.set(true) - laFailedAlert() - } - is LAResult.Unavailable -> { - m.performLA.value = false - prefPerformLA.set(false) - laUnavailableTurningOffAlert() - } - } - } - ) - } } -class SimplexViewModel(application: Application): AndroidViewModel(application) { - val app = getApplication<SimplexApp>() - val chatModel = app.chatModel -} - -@Composable -fun MainPage( - chatModel: ChatModel, - userAuthorized: MutableState<Boolean?>, - laFailed: MutableState<Boolean>, - destroyedAfterBackPress: MutableState<Boolean>, - runAuthenticate: () -> Unit, - setPerformLA: (Boolean) -> Unit, - showLANotice: () -> Unit -) { - var showChatDatabaseError by rememberSaveable { - mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null) - } - LaunchedEffect(chatModel.chatDbStatus.value) { - showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null - } - - var showAdvertiseLAAlert by remember { mutableStateOf(false) } - LaunchedEffect(showAdvertiseLAAlert) { - if ( - !chatModel.controller.appPrefs.laNoticeShown.get() - && showAdvertiseLAAlert - && chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete - && chatModel.chats.isNotEmpty() - && chatModel.activeCallInvitation.value == null - ) { - showLANotice() - } - } - LaunchedEffect(chatModel.showAdvertiseLAUnavailableAlert.value) { - if (chatModel.showAdvertiseLAUnavailableAlert.value) { - laUnavailableInstructionAlert() - } - } - LaunchedEffect(chatModel.clearOverlays.value) { - if (chatModel.clearOverlays.value) { - ModalManager.shared.closeModals() - chatModel.clearOverlays.value = false - } - } - - @Composable - fun AuthView() { - Surface(color = MaterialTheme.colors.background) { - Box( - Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - SimpleButton( - stringResource(MR.strings.auth_unlock), - icon = painterResource(MR.images.ic_lock), - click = { - laFailed.value = false - runAuthenticate() - } - ) - } - } - } - - Box { - val onboarding = chatModel.onboardingStage.value - val userCreated = chatModel.userCreated.value - var showInitializationView by remember { mutableStateOf(false) } - when { - chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView() - showChatDatabaseError -> { - chatModel.chatDbStatus.value?.let { - DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) - } - } - onboarding == null || userCreated == null -> SplashView() - onboarding == OnboardingStage.OnboardingComplete && userCreated -> { - Box { - showAdvertiseLAAlert = true - BoxWithConstraints { - var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } - val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } - Box( - Modifier - .graphicsLayer { - translationX = -offset.value.dp.toPx() - } - ) { - if (chatModel.setDeliveryReceipts.value) { - SetDeliveryReceiptsView(chatModel) - } else { - val stopped = chatModel.chatRunning.value == false - if (chatModel.sharedContent.value == null) - ChatListView(chatModel, setPerformLA, stopped) - else - ShareListView(chatModel, stopped) - } - } - val scope = rememberCoroutineScope() - val onComposed: () -> Unit = { - scope.launch { - offset.animateTo( - if (chatModel.chatId.value == null) 0f else maxWidth.value, - chatListAnimationSpec() - ) - if (offset.value == 0f) { - currentChatId = null - } - } - } - LaunchedEffect(Unit) { - launch { - snapshotFlow { chatModel.chatId.value } - .distinctUntilChanged() - .collect { - if (it != null) currentChatId = it - else onComposed() - } - } - } - Box (Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@ { - currentChatId?.let { - ChatView(it, chatModel, onComposed) - } - } - } - } - } - onboarding == OnboardingStage.Step1_SimpleXInfo -> SimpleXInfo(chatModel, onboarding = true) - onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {} - onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel) - onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) - } - ModalManager.shared.showInView() - val unauthorized = remember { derivedStateOf { userAuthorized.value != true } } - if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) { - LaunchedEffect(Unit) { - // With these constrains when user presses back button while on ChatList, activity destroys and shows auth request - // while the screen moves to a launcher. Detect it and prevent showing the auth - if (!(destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) { - runAuthenticate() - } - } - if (chatModel.controller.appPrefs.performLA.get() && laFailed.value) { - AuthView() - } else { - SplashView() - } - } else if (chatModel.showCallView.value) { - ActiveCallView(chatModel) - } - ModalManager.shared.showPasscodeInView() - val invitation = chatModel.activeCallInvitation.value - if (invitation != null) IncomingCallAlertView(invitation, chatModel) - AlertManager.shared.showInView() - - LaunchedEffect(Unit) { - delay(1000) - if (chatModel.chatDbStatus.value == null) { - showInitializationView = true - } - } - } - - DisposableEffectOnRotate { - // When using lock delay = 0 and screen rotates, the app will be locked which is not useful. - // Let's prolong the unlocked period to 3 sec for screen rotation to take place - if (chatModel.controller.appPrefs.laLockDelay.get() == 0) { - enteredBackground.value = elapsedRealtime() + 3000 - } - } -} - -@Composable -private fun InitializationView() { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - CircularProgressIndicator( - Modifier - .padding(bottom = DEFAULT_PADDING) - .size(30.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 2.5.dp - ) - Text(stringResource(MR.strings.opening_database)) - } - } -} - -fun processNotificationIntent(intent: Intent?, chatModel: ChatModel) { +fun processNotificationIntent(intent: Intent?) { val userId = getUserIdFromIntent(intent) when (intent?.action) { NtfManager.OpenChatAction -> { val chatId = intent.getStringExtra("chatId") Log.d(TAG, "processNotificationIntent: OpenChatAction $chatId") if (chatId != null) { - withBGApi { - awaitChatStartedIfNeeded(chatModel) - if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { - chatModel.controller.changeActiveUser(userId, null) - } - val cInfo = chatModel.getChat(chatId)?.chatInfo - chatModel.clearOverlays.value = true - if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel) - } + ntfManager.openChatAction(userId, chatId) } } NtfManager.ShowChatsAction -> { Log.d(TAG, "processNotificationIntent: ShowChatsAction") - withBGApi { - awaitChatStartedIfNeeded(chatModel) - if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { - chatModel.controller.changeActiveUser(userId, null) - } - chatModel.chatId.value = null - chatModel.clearOverlays.value = true - } + ntfManager.showChatsAction(userId) } NtfManager.AcceptCallAction -> { val chatId = intent.getStringExtra("chatId") if (chatId == null || chatId == "") return Log.d(TAG, "processNotificationIntent: AcceptCallAction $chatId") - chatModel.clearOverlays.value = true - val invitation = chatModel.callInvitations[chatId] - if (invitation == null) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) - } else { - chatModel.callManager.acceptIncomingCall(invitation = invitation) - } + ntfManager.acceptCallAction(chatId) } } } -fun processIntent(intent: Intent?, chatModel: ChatModel) { +fun processIntent(intent: Intent?) { when (intent?.action) { "android.intent.action.VIEW" -> { val uri = intent.data - if (uri != null) connectIfOpenedViaUri(uri, chatModel) + if (uri != null) connectIfOpenedViaUri(uri.toURI(), ChatModel) } } } -fun processExternalIntent(intent: Intent?, chatModel: ChatModel) { +fun processExternalIntent(intent: Intent?) { when (intent?.action) { Intent.ACTION_SEND -> { // Close active chat and show a list of chats @@ -633,20 +141,25 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) { when { intent.type == "text/plain" -> { val text = intent.getStringExtra(Intent.EXTRA_TEXT) - if (text != null) { + val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri + if (uri != null) { + // Shared file that contains plain text, like `*.log` file + chatModel.sharedContent.value = SharedContent.File(text ?: "", uri.toURI()) + } else if (text != null) { + // Shared just a text chatModel.sharedContent.value = SharedContent.Text(text) } } isMediaIntent(intent) -> { val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri if (uri != null) { - chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri)) + chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", listOf(uri.toURI())) } // All other mime types } else -> { val uri = intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri if (uri != null) { - chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri) + chatModel.sharedContent.value = SharedContent.File(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uri.toURI()) } } } @@ -660,7 +173,7 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) { isMediaIntent(intent) -> { val uris = intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM) as? List<Uri> if (uris != null) { - chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris) + chatModel.sharedContent.value = SharedContent.Media(intent.getStringExtra(Intent.EXTRA_TEXT) ?: "", uris.map { it.toURI() }) } // All other mime types } else -> {} @@ -672,48 +185,6 @@ fun processExternalIntent(intent: Intent?, chatModel: ChatModel) { fun isMediaIntent(intent: Intent): Boolean = intent.type?.startsWith("image/") == true || intent.type?.startsWith("video/") == true -fun connectIfOpenedViaUri(uri: Uri, chatModel: ChatModel) { - Log.d(TAG, "connectIfOpenedViaUri: opened via link") - if (chatModel.currentUser.value == null) { - chatModel.appOpenUrl.value = uri - } else { - withUriAction(uri) { linkType -> - val title = when (linkType) { - ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link) - ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link) - ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link) - } - AlertManager.shared.showAlertDialog( - title = title, - text = if (linkType == ConnectionLinkType.GROUP) - generalGetString(MR.strings.you_will_join_group) - else - generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link), - confirmText = generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { - withApi { - Log.d(TAG, "connectIfOpenedViaUri: connecting") - connectViaUri(chatModel, linkType, uri) - } - } - ) - } - } -} - -suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) { - // Still decrypting database - if (chatModel.chatRunning.value == null) { - val step = 50L - for (i in 0..(timeout / step)) { - if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) { - break - } - delay(step) - } - } -} - //fun testJson() { // val str: String = """ // """.trimIndent() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt similarity index 94% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt rename to apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt index 5b18ff5220..3b8902b58c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/MessagesFetcherWorker.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt @@ -1,10 +1,13 @@ -package chat.simplex.app.views.helpers +package chat.simplex.app import android.content.Context import android.util.Log import androidx.work.* import chat.simplex.app.* import chat.simplex.app.SimplexService.Companion.showPassphraseNotification +import chat.simplex.common.model.ChatController +import chat.simplex.common.views.helpers.DBMigrationResult +import chat.simplex.app.BuildConfig import kotlinx.coroutines.* import java.util.Date import java.util.concurrent.TimeUnit @@ -55,7 +58,7 @@ class MessagesFetcherWork( var shouldReschedule = true try { withTimeout(durationSeconds * 1000L) { - val chatController = (applicationContext as SimplexApp).chatController + val chatController = ChatController SimplexService.waitDbMigrationEnds(chatController) val chatDbStatus = chatController.chatModel.chatDbStatus.value if (chatDbStatus != DBMigrationResult.OK) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 209e1f4093..f70032788b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -1,101 +1,31 @@ package chat.simplex.app import android.app.Application -import android.net.LocalServerSocket -import android.util.Log +import chat.simplex.common.platform.Log import androidx.lifecycle.* import androidx.work.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.DefaultTheme -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.OnboardingStage -import chat.simplex.app.views.usersettings.NotificationsMode +import chat.simplex.app.model.NtfManager +import chat.simplex.common.helpers.APPLICATION_ID +import chat.simplex.common.helpers.requiresIgnoringBattery +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.common.platform.* +import chat.simplex.common.views.call.RcvCallInvitation import com.jakewharton.processphoenix.ProcessPhoenix import kotlinx.coroutines.* -import kotlinx.serialization.decodeFromString import java.io.* -import java.lang.ref.WeakReference import java.util.* -import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread const val TAG = "SIMPLEX" -// ghc's rts -external fun initHS() -// android-support -external fun pipeStdOutToSocket(socketName: String) : Int - -// SimpleX API -typealias ChatCtrl = Long -external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any> -external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String -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 chatPasswordHash(pwd: String, salt: String): String - class SimplexApp: Application(), LifecycleEventObserver { - var mainActivity: WeakReference<MainActivity> = WeakReference(null) val chatModel: ChatModel get() = chatController.chatModel - val appPreferences: AppPreferences - get() = chatController.appPrefs val chatController: ChatController = ChatController - var isAppOnForeground: Boolean = false - - val defaultLocale: Locale = Locale.getDefault() - - suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { - val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() - val dbAbsolutePathPrefix = getFilesDirectory() - val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp - val migrated: Array<Any> = chatMigrateInit(dbAbsolutePathPrefix, dbKey, confirm.value) - val res: DBMigrationResult = kotlin.runCatching { - json.decodeFromString<DBMigrationResult>(migrated[0] as String) - }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } - val ctrl = if (res is DBMigrationResult.OK) { - migrated[1] as Long - } else null - chatController.ctrl = ctrl - chatModel.chatDbEncrypted.value = dbKey != "" - chatModel.chatDbStatus.value = res - if (res != DBMigrationResult.OK) { - Log.d(TAG, "Unable to migrate successfully: $res") - } else if (startChat) { - // If we migrated successfully means previous re-encryption process on database level finished successfully too - if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) - val user = chatController.apiGetActiveUser() - if (user == null) { - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) - chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo - chatModel.currentUser.value = null - chatModel.users.clear() - } else { - val savedOnboardingStage = appPreferences.onboardingStage.get() - chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { - OnboardingStage.Step3_CreateSimpleXAddress - } else { - savedOnboardingStage - } - if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { - chatModel.setDeliveryReceipts.value = true - } - chatController.startChat(user) - // Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet - if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { - SimplexService.showBackgroundServiceNoticeIfNeeded() - if (appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name) - SimplexService.start() - } - } - } - } - override fun onCreate() { super.onCreate() @@ -103,7 +33,11 @@ class SimplexApp: Application(), LifecycleEventObserver { return; } context = this - context.getDir("temp", MODE_PRIVATE).deleteRecursively() + initHaskell() + initMultiplatform() + tmpDir.deleteRecursively() + tmpDir.mkdir() + withBGApi { initChatController() runMigrations() @@ -137,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver { } Lifecycle.Event.ON_RESUME -> { isAppOnForeground = true - if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) { + if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { SimplexService.showBackgroundServiceNoticeIfNeeded() } /** @@ -146,8 +80,8 @@ class SimplexApp: Application(), LifecycleEventObserver { * It can happen when app was started and a user enables battery optimization while app in background * */ if (chatModel.chatRunning.value != false && - chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && - appPreferences.notificationsMode.get() == NotificationsMode.SERVICE.name + chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete && + appPrefs.notificationsMode.get() == NotificationsMode.SERVICE ) { SimplexService.start() } @@ -158,13 +92,13 @@ class SimplexApp: Application(), LifecycleEventObserver { } fun allowToStartServiceAfterAppExit() = with(chatModel.controller) { - appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name && - (!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations()) + appPrefs.notificationsMode.get() == NotificationsMode.SERVICE && + (!NotificationsMode.SERVICE.requiresIgnoringBattery || SimplexService.isBackgroundAllowed()) } private fun allowToStartPeriodically() = with(chatModel.controller) { - appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC.name && - (!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isIgnoringBatteryOptimizations()) + appPrefs.notificationsMode.get() == NotificationsMode.PERIODIC && + (!NotificationsMode.PERIODIC.requiresIgnoringBattery || SimplexService.isBackgroundAllowed()) } /* @@ -198,75 +132,86 @@ class SimplexApp: Application(), LifecycleEventObserver { MessagesFetcherWorker.scheduleWork() } - private fun runMigrations() { - val lastMigration = chatModel.controller.appPrefs.lastMigratedVersionCode - if (lastMigration.get() < BuildConfig.VERSION_CODE) { - while (true) { - if (lastMigration.get() < 117) { - if (chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.DARK.name) { - chatModel.controller.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) - } - lastMigration.set(117) - } else { - lastMigration.set(BuildConfig.VERSION_CODE) - break - } - } - } - } - companion object { lateinit var context: SimplexApp private set + } - init { - val socketName = BuildConfig.APPLICATION_ID + ".local.socket.address.listen.native.cmd2" - val s = Semaphore(0) - thread(name="stdout/stderr pipe") { - Log.d(TAG, "starting server") - var server: LocalServerSocket? = null - for (i in 0..100) { - try { - server = LocalServerSocket(socketName + i) - break - } catch (e: IOException) { - Log.e(TAG, e.stackTraceToString()) - } + private fun initMultiplatform() { + androidAppContext = this + APPLICATION_ID = BuildConfig.APPLICATION_ID + ntfManager = object : chat.simplex.common.platform.NtfManager() { + override fun notifyCallInvitation(invitation: RcvCallInvitation) = NtfManager.notifyCallInvitation(invitation) + override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId) + override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId) + override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first }) + override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() + override fun cancelCallNotification() = NtfManager.cancelCallNotification() + override fun cancelAllNotifications() = NtfManager.cancelAllNotifications() + } + platform = object : PlatformInterface { + override suspend fun androidServiceStart() { + SimplexService.start() + } + + override fun androidServiceSafeStop() { + SimplexService.safeStopService() + } + + override fun androidNotificationsModeChanged(mode: NotificationsMode) { + if (mode.requiresIgnoringBattery && !SimplexService.isBackgroundAllowed()) { + appPrefs.backgroundServiceNoticeShown.set(false) } - if (server == null) { - throw Error("Unable to setup local server socket. Contact developers") + SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) + CoroutineScope(Dispatchers.Default).launch { + if (mode == NotificationsMode.SERVICE) + SimplexService.start() + else + SimplexService.safeStopService() } - Log.d(TAG, "started server") - s.release() - val receiver = server.accept() - Log.d(TAG, "started receiver") - val logbuffer = FifoQueue<String>(500) - if (receiver != null) { - val inStream = receiver.inputStream - val inStreamReader = InputStreamReader(inStream) - val input = BufferedReader(inStreamReader) - Log.d(TAG, "starting receiver loop") - while (true) { - val line = input.readLine() ?: break - Log.w("$TAG (stdout/stderr)", line) - logbuffer.add(line) - } - Log.w(TAG, "exited receiver loop") + + if (mode != NotificationsMode.PERIODIC) { + MessagesFetcherWorker.cancelAll() + } + SimplexService.showBackgroundServiceNoticeIfNeeded(showOffAlert = false) + } + + override fun androidChatStartedAfterBeingOff() { + SimplexService.cancelPassphraseNotification() + when (appPrefs.notificationsMode.get()) { + NotificationsMode.SERVICE -> CoroutineScope(Dispatchers.Default).launch { platform.androidServiceStart() } + NotificationsMode.PERIODIC -> SimplexApp.context.schedulePeriodicWakeUp() + NotificationsMode.OFF -> {} } } - System.loadLibrary("app-lib") + override fun androidChatStopped() { + SimplexService.safeStopService() + MessagesFetcherWorker.cancelAll() + } - s.acquire() - pipeStdOutToSocket(socketName) + override fun androidChatInitializedAndStarted() { + // Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet + if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { + SimplexService.showBackgroundServiceNoticeIfNeeded() + if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE) + withBGApi { + platform.androidServiceStart() + } + } + } - initHS() + override fun androidIsBackgroundCallAllowed(): Boolean = !SimplexService.isBackgroundRestricted() + + override suspend fun androidAskToAllowBackgroundCalls(): Boolean { + if (SimplexService.isBackgroundRestricted()) { + val userChoice: CompletableDeferred<Boolean> = CompletableDeferred() + SimplexService.showBGRestrictedInCall { + userChoice.complete(it) + } + return userChoice.await() + } + return true + } } } } - -class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() { - override fun add(element: E): Boolean { - if(size > capacity) removeFirst() - return super.add(element) - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index 202471919f..2cb6c12da0 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -7,23 +7,26 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.* import android.provider.Settings -import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.work.* -import chat.simplex.app.model.ChatController -import chat.simplex.app.model.ChatModel -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.NotificationsMode +import chat.simplex.common.AppLock +import chat.simplex.common.helpers.requiresIgnoringBattery +import chat.simplex.common.model.ChatController +import chat.simplex.common.model.NotificationsMode +import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.views.helpers.* +import kotlinx.coroutines.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import kotlinx.coroutines.* // based on: // https://robertohuertas.com/2019/06/29/android_foreground_services/ @@ -47,6 +50,7 @@ class SimplexService: Service() { } else { Log.d(TAG, "null intent. Probably restarted by the system.") } + startForeground(SIMPLEX_SERVICE_ID, serviceNotification) return START_STICKY // to restart if killed } @@ -97,7 +101,7 @@ class SimplexService: Service() { val self = this isStartingService = true withApi { - val chatController = (application as SimplexApp).chatController + val chatController = ChatController waitDbMigrationEnds(chatController) try { Log.w(TAG, "Starting foreground service") @@ -105,7 +109,7 @@ class SimplexService: Service() { if (chatDbStatus != DBMigrationResult.OK) { Log.w(chat.simplex.app.TAG, "SimplexService: problem with the database: $chatDbStatus") showPassphraseNotification(chatDbStatus) - safeStopService(self) + safeStopService() return@withApi } saveServiceState(self, ServiceState.STARTED) @@ -167,7 +171,7 @@ class SimplexService: Service() { // re-schedules the task when "Clear recent apps" is pressed override fun onTaskRemoved(rootIntent: Intent) { // Just to make sure that after restart of the app the user will need to re-authenticate - MainActivity.clearAuthState() + AppLock.clearAuthState() // If notification service isn't enabled or battery optimization isn't disabled, we shouldn't restart the service if (!SimplexApp.context.allowToStartServiceAfterAppExit()) { @@ -265,9 +269,9 @@ class SimplexService: Service() { * If there is a need to stop the service, use this function only. It makes sure that the service will be stopped without an * exception related to foreground services lifecycle * */ - fun safeStopService(context: Context) { + fun safeStopService() { if (isServiceStarted) { - context.stopService(Intent(context, SimplexService::class.java)) + androidAppContext.stopService(Intent(androidAppContext, SimplexService::class.java)) } else { stopAfterStart = true } @@ -276,9 +280,9 @@ class SimplexService: Service() { private suspend fun serviceAction(action: Action) { Log.d(TAG, "SimplexService serviceAction: ${action.name}") withContext(Dispatchers.IO) { - Intent(SimplexApp.context, SimplexService::class.java).also { + Intent(androidAppContext, SimplexService::class.java).also { it.action = action.name - ContextCompat.startForegroundService(SimplexApp.context, it) + ContextCompat.startForegroundService(androidAppContext, it) } } } @@ -350,37 +354,37 @@ class SimplexService: Service() { private fun getPreferences(context: Context): SharedPreferences = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) - fun showBackgroundServiceNoticeIfNeeded() { + fun showBackgroundServiceNoticeIfNeeded(showOffAlert: Boolean = true) { val appPrefs = ChatController.appPrefs - val mode = NotificationsMode.valueOf(appPrefs.notificationsMode.get()!!) + val mode = appPrefs.notificationsMode.get() Log.d(TAG, "showBackgroundServiceNoticeIfNeeded") // Nothing to do if mode is OFF. Can be selected on on-boarding stage if (mode == NotificationsMode.OFF) return if (!appPrefs.backgroundServiceNoticeShown.get()) { // the branch for the new users who have never seen service notice - if (!mode.requiresIgnoringBattery || isIgnoringBatteryOptimizations()) { + if (!mode.requiresIgnoringBattery || isBackgroundAllowed()) { showBGServiceNotice(mode) - } else { - showBGServiceNoticeIgnoreOptimization(mode) + } else if (isBackgroundRestricted()) { + showBGServiceNoticeSystemRestricted(mode, showOffAlert) + } else if (!isIgnoringBatteryOptimizations()) { + showBGServiceNoticeIgnoreOptimization(mode, showOffAlert) } // set both flags, so that if the user doesn't allow ignoring optimizations, the service will be disabled without additional notice appPrefs.backgroundServiceNoticeShown.set(true) appPrefs.backgroundServiceBatteryNoticeShown.set(true) + } else if (mode.requiresIgnoringBattery && isBackgroundRestricted()) { + // the branch for users who have app installed, and have seen the service notice, + // but the service is running AND system background restriction is on OR the battery optimization for the app is in RESTRICTED state + showBGServiceNoticeSystemRestricted(mode, showOffAlert) + if (!appPrefs.backgroundServiceBatteryNoticeShown.get()) { + appPrefs.backgroundServiceBatteryNoticeShown.set(true) + } } else if (mode.requiresIgnoringBattery && !isIgnoringBatteryOptimizations()) { // the branch for users who have app installed, and have seen the service notice, - // but the battery optimization for the app is on (Android 12) AND the service is running - if (appPrefs.backgroundServiceBatteryNoticeShown.get()) { - // users have been presented with battery notice before - they did not allow ignoring optimizations -> disable service - showDisablingServiceNotice(mode) - appPrefs.notificationsMode.set(NotificationsMode.OFF.name) - ChatModel.notificationsMode.value = NotificationsMode.OFF - SimplexService.StartReceiver.toggleReceiver(false) - MessagesFetcherWorker.cancelAll() - SimplexService.safeStopService(SimplexApp.context) - } else { - // show battery optimization notice - showBGServiceNoticeIgnoreOptimization(mode) + // but the battery optimization for the app is in OPTIMIZED state (Android 12+) AND the service is running + showBGServiceNoticeIgnoreOptimization(mode, showOffAlert) + if (!appPrefs.backgroundServiceBatteryNoticeShown.get()) { appPrefs.backgroundServiceBatteryNoticeShown.set(true) } } else { @@ -423,13 +427,17 @@ class SimplexService: Service() { ) } - private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode) = AlertManager.shared.showAlert { + private fun showBGServiceNoticeIgnoreOptimization(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert { val ignoreOptimization = { AlertManager.shared.hideAlert() askAboutIgnoringBatteryOptimization() } + val disableNotifications = { + AlertManager.shared.hideAlert() + disableNotifications(mode, showOffAlert) + } AlertDialog( - onDismissRequest = ignoreOptimization, + onDismissRequest = disableNotifications, title = { Row { Icon( @@ -452,12 +460,98 @@ class SimplexService: Service() { Text(annotatedStringResource(MR.strings.turn_off_battery_optimization)) } }, + dismissButton = { + TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) } + }, confirmButton = { - TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.ok)) } + TextButton(onClick = ignoreOptimization) { Text(stringResource(MR.strings.turn_off_battery_optimization_button)) } } ) } + private fun showBGServiceNoticeSystemRestricted(mode: NotificationsMode, showOffAlert: Boolean) = AlertManager.shared.showAlert { + val unrestrict = { + AlertManager.shared.hideAlert() + askToUnrestrictBackground() + } + val disableNotifications = { + AlertManager.shared.hideAlert() + disableNotifications(mode, showOffAlert) + } + AlertDialog( + onDismissRequest = disableNotifications, + title = { + Row { + Icon( + painterResource(MR.images.ic_bolt), + contentDescription = + if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.icon_descr_instant_notifications) else stringResource(MR.strings.periodic_notifications), + ) + Text( + if (mode == NotificationsMode.SERVICE) stringResource(MR.strings.service_notifications) else stringResource(MR.strings.periodic_notifications), + fontWeight = FontWeight.Bold + ) + } + }, + text = { + Column { + Text( + annotatedStringResource(MR.strings.system_restricted_background_desc), + Modifier.padding(bottom = 8.dp) + ) + Text(annotatedStringResource(MR.strings.system_restricted_background_warn)) + } + }, + dismissButton = { + TextButton(onClick = disableNotifications) { Text(stringResource(MR.strings.disable_notifications_button), color = MaterialTheme.colors.error) } + }, + confirmButton = { + TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) } + } + ) + } + + fun showBGRestrictedInCall(onDismiss: (allowedCall: Boolean) -> Unit) = AlertManager.shared.showAlert { + val unrestrict = { + askToUnrestrictBackground() + } + AlertDialog( + onDismissRequest = AlertManager.shared::hideAlert, + title = { + Text( + stringResource(MR.strings.system_restricted_background_in_call_title), + fontWeight = FontWeight.Bold + ) + }, + text = { + Column { + Text( + annotatedStringResource(MR.strings.system_restricted_background_in_call_desc), + Modifier.padding(bottom = 8.dp) + ) + Text(annotatedStringResource(MR.strings.system_restricted_background_in_call_warn)) + } + }, + confirmButton = { + TextButton(onClick = unrestrict) { Text(stringResource(MR.strings.turn_off_system_restriction_button)) } + } + ) + val scope = rememberCoroutineScope() + DisposableEffect(Unit) { + scope.launch { + repeat(10000) { + delay(200) + if (!isBackgroundRestricted()) { + AlertManager.shared.hideAlert() + } + } + } + onDispose { + onDismiss(!isBackgroundRestricted()) + } + } + } + private fun showDisablingServiceNotice(mode: NotificationsMode) = AlertManager.shared.showAlert { AlertDialog( onDismissRequest = AlertManager.shared::hideAlert, @@ -488,20 +582,55 @@ class SimplexService: Service() { ) } + fun isBackgroundAllowed(): Boolean = isIgnoringBatteryOptimizations() && !isBackgroundRestricted() + fun isIgnoringBatteryOptimizations(): Boolean { - val powerManager = SimplexApp.context.getSystemService(Application.POWER_SERVICE) as PowerManager - return powerManager.isIgnoringBatteryOptimizations(SimplexApp.context.packageName) + val powerManager = androidAppContext.getSystemService(Application.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(androidAppContext.packageName) + } + + fun isBackgroundRestricted(): Boolean { + return if (Build.VERSION.SDK_INT >= 28) { + val activityService = androidAppContext.getSystemService(ACTIVITY_SERVICE) as ActivityManager + activityService.isBackgroundRestricted + } else { + false + } } private fun askAboutIgnoringBatteryOptimization() { Intent().apply { @SuppressLint("BatteryLife") action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:${SimplexApp.context.packageName}") + data = Uri.parse("package:${androidAppContext.packageName}") // This flag is needed when you start a new activity from non-Activity context addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - SimplexApp.context.startActivity(this) + androidAppContext.startActivity(this) } } + + private fun askToUnrestrictBackground() { + Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.parse("package:${androidAppContext.packageName}") + // This flag is needed when you start a new activity from non-Activity context + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + androidAppContext.startActivity(this) + } catch (e: ActivityNotFoundException) { + Log.e(TAG, e.stackTraceToString()) + } + } + } + + private fun disableNotifications(mode: NotificationsMode, showOffAlert: Boolean) { + if (showOffAlert) { + showDisablingServiceNotice(mode) + } + ChatController.appPrefs.notificationsMode.set(NotificationsMode.OFF) + StartReceiver.toggleReceiver(false) + MessagesFetcherWorker.cancelAll() + safeStopService() + } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt similarity index 82% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.kt rename to apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index f3b7e7a180..916f40df13 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -1,6 +1,5 @@ package chat.simplex.app.model -import android.Manifest import android.app.* import android.app.TaskStackBuilder import android.content.* @@ -9,16 +8,20 @@ import android.graphics.BitmapFactory import android.hardware.display.DisplayManager import android.media.AudioAttributes import android.net.Uri -import android.util.Log import android.view.Display +import androidx.compose.ui.graphics.asAndroidBitmap import androidx.core.app.* import chat.simplex.app.* -import chat.simplex.app.views.call.* -import chat.simplex.app.views.chatlist.acceptContactRequest -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.NotificationPreviewMode -import chat.simplex.res.MR +import chat.simplex.app.TAG +import chat.simplex.app.views.call.IncomingCallActivity +import chat.simplex.app.views.call.getKeyguardManager +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.call.RcvCallInvitation import kotlinx.datetime.Clock +import chat.simplex.res.MR object NtfManager { const val MessageChannel: String = "chat.simplex.app.MESSAGE_NOTIFICATION" @@ -33,7 +36,7 @@ object NtfManager { const val CallNotificationId: Int = -1 private const val UserIdKey: String = "userId" private const val ChatIdKey: String = "chatId" - private val appPreferences: AppPreferences by lazy { ChatController.appPrefs } + private val appPreferences: AppPreferences = ChatController.appPrefs private val context: Context get() = SimplexApp.context @@ -42,7 +45,7 @@ object NtfManager { return if (userId == -1L || userId == null) null else userId } - private val manager: NotificationManager = SimplexApp.context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private var prevNtfTime = mutableMapOf<String, Long>() private val msgNtfTimeoutMs = 30000L @@ -50,10 +53,6 @@ object NtfManager { if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert() } - enum class NotificationAction { - ACCEPT_CONTACT_REQUEST - } - private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel { val callChannel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH) val attrs = AudioAttributes.Builder() @@ -82,32 +81,7 @@ object NtfManager { } } - fun notifyContactRequestReceived(user: User, cInfo: ChatInfo.ContactRequest) { - displayNotification( - user = user, - chatId = cInfo.id, - displayName = cInfo.displayName, - msgText = generalGetString(MR.strings.notification_new_contact_request), - image = cInfo.image, - listOf(NotificationAction.ACCEPT_CONTACT_REQUEST) - ) - } - - fun notifyContactConnected(user: User, contact: Contact) { - displayNotification( - user = user, - chatId = contact.id, - displayName = contact.displayName, - msgText = generalGetString(MR.strings.notification_contact_connected) - ) - } - - fun notifyMessageReceived(user: User, cInfo: ChatInfo, cItem: ChatItem) { - if (!cInfo.ntfsEnabled) return - displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) - } - - fun displayNotification(user: User, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) { + fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<NotificationAction> = emptyList()) { if (!user.showNotifications) return Log.d(TAG, "notifyMessageReceived $chatId") val now = Clock.System.now().toEpochMilliseconds() @@ -119,7 +93,7 @@ object NtfManager { val largeIcon = when { actions.isEmpty() -> null image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> BitmapFactory.decodeResource(context.resources, R.drawable.icon) - else -> base64ToBitmap(image) + else -> base64ToBitmap(image).asAndroidBitmap() } val builder = NotificationCompat.Builder(context, MessageChannel) .setContentTitle(title) @@ -144,6 +118,7 @@ object NtfManager { val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags) val actionButton = when (action) { NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(MR.strings.accept) + NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO -> generalGetString(MR.strings.accept_contact_incognito_button) } builder.addAction(0, actionButton, actionPendingIntent) } @@ -158,7 +133,7 @@ object NtfManager { with(NotificationManagerCompat.from(context)) { // using cInfo.id only shows one notification per chat and updates it when the message arrives - if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { notify(chatId.hashCode(), builder.build()) notify(0, summary) } @@ -172,9 +147,9 @@ object NtfManager { "notifyCallInvitation pre-requests: " + "keyguard locked ${keyguardManager.isKeyguardLocked}, " + "callOnLockScreen ${appPreferences.callOnLockScreen.get()}, " + - "onForeground ${SimplexApp.context.isAppOnForeground}" + "onForeground ${isAppOnForeground}" ) - if (SimplexApp.context.isAppOnForeground) return + if (isAppOnForeground) return val contactId = invitation.contact.id Log.d(TAG, "notifyCallInvitation $contactId") val image = invitation.contact.image @@ -212,7 +187,7 @@ object NtfManager { val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name) BitmapFactory.decodeResource(context.resources, R.drawable.icon) else - base64ToBitmap(image) + base64ToBitmap(image).asAndroidBitmap() ntfBuilder = ntfBuilder .setContentTitle(title) @@ -227,7 +202,7 @@ object NtfManager { // This makes notification sound and vibration repeat endlessly notification.flags = notification.flags or NotificationCompat.FLAG_INSISTENT with(NotificationManagerCompat.from(context)) { - if (ActivityCompat.checkSelfPermission(SimplexApp.context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { + if (ActivityCompat.checkSelfPermission(SimplexApp.context, android.Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) { notify(CallNotificationId, notification) } } @@ -243,19 +218,6 @@ object NtfManager { fun hasNotificationsForChat(chatId: String): Boolean = manager.activeNotifications.any { it.id == chatId.hashCode() } - private fun hideSecrets(cItem: ChatItem): String { - val md = cItem.formattedText - return if (md != null) { - var res = "" - for (ft in md) { - res += if (ft.format is Format.Secret) "..." else ft.text - } - res - } else { - cItem.text - } - } - private fun chatPendingIntent(intentAction: String, userId: Long?, chatId: String? = null, broadcast: Boolean = false): PendingIntent { Log.d(TAG, "chatPendingIntent for $intentAction") val uniqueInt = (System.currentTimeMillis() and 0xfffffff).toInt() @@ -299,18 +261,8 @@ object NtfManager { val chatId = intent?.getStringExtra(ChatIdKey) ?: return val m = SimplexApp.context.chatModel when (intent.action) { - NotificationAction.ACCEPT_CONTACT_REQUEST.name -> { - val isCurrentUser = m.currentUser.value?.userId == userId - val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) { - (m.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return - } else { - null - } - val apiId = chatId.replace("<@", "").toLongOrNull() ?: return - acceptContactRequest(apiId, cInfo, isCurrentUser, m) - cancelNotificationsForChat(chatId) - } - + NotificationAction.ACCEPT_CONTACT_REQUEST.name -> ntfManager.acceptContactRequestAction(userId, incognito = false, chatId) + NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO.name -> ntfManager.acceptContactRequestAction(userId, incognito = true, chatId) RejectCallAction -> { val invitation = m.callInvitations[chatId] if (invitation != null) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt index d2ffc507e0..104b654c58 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallActivity.kt @@ -4,16 +4,14 @@ import android.app.Activity import android.app.KeyguardManager import android.content.Context import android.content.Intent -import android.content.res.Configuration import android.os.Build import android.os.Bundle -import android.util.Log +import chat.simplex.common.platform.Log import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.activity.viewModels +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -24,27 +22,27 @@ import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.* import chat.simplex.app.R -import chat.simplex.app.model.* +import chat.simplex.common.model.* import chat.simplex.app.model.NtfManager.OpenChatAction -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ProfileImage +import chat.simplex.common.platform.ntfManager +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock class IncomingCallActivity: ComponentActivity() { - private val vm by viewModels<SimplexViewModel>() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { IncomingCallActivityView(vm.chatModel) } + setContent { IncomingCallActivityView(ChatModel) } unlockForIncomingCall() } @@ -103,7 +101,7 @@ fun IncomingCallActivityView(m: ChatModel) { ) { if (showCallView) { Box { - ActiveCallView(m) + ActiveCallView() if (invitation != null) IncomingCallAlertView(invitation, m) } } else if (invitation != null) { @@ -121,7 +119,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo DisposableEffect(Unit) { onDispose { // Cancel notification whatever happens next since otherwise sound from notification and from inside the app can co-exist - chatModel.controller.ntfManager.cancelCallNotification() + ntfManager.cancelCallNotification() } } IncomingCallLockScreenAlertLayout( @@ -131,7 +129,7 @@ fun IncomingCallLockScreenAlert(invitation: RcvCallInvitation, chatModel: ChatMo rejectCall = { cm.endCall(invitation = invitation) }, ignoreCall = { chatModel.activeCallInvitation.value = null - chatModel.controller.ntfManager.cancelCallNotification() + ntfManager.cancelCallNotification() }, acceptCall = { cm.acceptIncomingCall(invitation = invitation) }, openApp = { @@ -171,18 +169,18 @@ fun IncomingCallLockScreenAlertLayout( Text(invitation.contact.chatViewName, style = MaterialTheme.typography.h2) Spacer(Modifier.fillMaxHeight().weight(1f)) Row { - LockScreenCallButton(stringResource(MR.strings.reject), painterResource(MR.images.ic_call_end_filled), Color.Red, rejectCall) + LockScreenCallButton(stringResource(MR.strings.reject), painterResource(R.drawable.ic_call_end_filled), Color.Red, rejectCall) Spacer(Modifier.size(48.dp)) - LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(MR.images.ic_close), MaterialTheme.colors.primary, ignoreCall) + LockScreenCallButton(stringResource(MR.strings.ignore), painterResource(R.drawable.ic_close), MaterialTheme.colors.primary, ignoreCall) Spacer(Modifier.size(48.dp)) - LockScreenCallButton(stringResource(MR.strings.accept), painterResource(MR.images.ic_check_filled), SimplexGreen, acceptCall) + LockScreenCallButton(stringResource(MR.strings.accept), painterResource(R.drawable.ic_check_filled), SimplexGreen, acceptCall) } } else if (callOnLockScreen == CallOnLockScreen.SHOW) { SimpleXLogo() Text(stringResource(MR.strings.open_simplex_chat_to_accept_call), textAlign = TextAlign.Center, lineHeight = 22.sp) Text(stringResource(MR.strings.allow_accepting_calls_from_lock_screen), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2, lineHeight = 22.sp) Spacer(Modifier.fillMaxHeight().weight(1f)) - SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(MR.images.ic_check_filled), click = openApp) + SimpleButton(text = stringResource(MR.strings.open_verb), icon = painterResource(R.drawable.ic_check_filled), click = openApp) } } } @@ -190,7 +188,7 @@ fun IncomingCallLockScreenAlertLayout( @Composable private fun SimpleXLogo() { Image( - painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo), + painter = painterResource(if (isInDarkTheme()) R.drawable.logo_light else R.drawable.logo), contentDescription = stringResource(MR.strings.image_descr_simplex_logo), modifier = Modifier .padding(vertical = DEFAULT_PADDING) @@ -219,10 +217,10 @@ private fun LockScreenCallButton(text: String, icon: Painter, color: Color, acti } } -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true -) +)*/ @Composable fun PreviewIncomingCallLockScreenAlert() { SimpleXTheme(true) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt deleted file mode 100644 index 32933a57d7..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIEventView.kt +++ /dev/null @@ -1,61 +0,0 @@ -package chat.simplex.app.views.chat.item - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.* - -@Composable -fun CIEventView(ci: ChatItem) { - @Composable - fun chatEventTextView(text: AnnotatedString) { - Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) - } - Row( - Modifier.padding(horizontal = 6.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val memberDisplayName = ci.memberDisplayName - if (memberDisplayName != null) { - chatEventTextView( - buildAnnotatedString { - withStyle(chatEventStyle) { append(memberDisplayName) } - append(" ") - }.plus(chatEventText(ci)) - ) - } else { - chatEventTextView(chatEventText(ci)) - } - } -} - -val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) - -fun chatEventText(ci: ChatItem): AnnotatedString = - buildAnnotatedString { - withStyle(chatEventStyle) { append(ci.content.text + " " + ci.timestampText) } - } - -@Preview(showBackground = true) -@Preview( - uiMode = Configuration.UI_MODE_NIGHT_YES, - name = "Dark Mode" -) -@Composable -fun CIEventViewPreview() { - SimpleXTheme { - CIEventView( - ChatItem.getGroupEventSample() - ) - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ChooseAttachmentView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ChooseAttachmentView.kt deleted file mode 100644 index 38b2f0e219..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ChooseAttachmentView.kt +++ /dev/null @@ -1,62 +0,0 @@ -package chat.simplex.app.views.helpers - -import androidx.compose.foundation.layout.* -import androidx.compose.material.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.views.newchat.ActionButton -import chat.simplex.res.MR - -sealed class AttachmentOption { - object CameraPhoto: AttachmentOption() - object GalleryImage: AttachmentOption() - object GalleryVideo: AttachmentOption() - object File: AttachmentOption() -} - -@Composable -fun ChooseAttachmentView( - attachmentOption: MutableState<AttachmentOption?>, - hide: () -> Unit -) { - Box( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .onFocusChanged { focusState -> - if (!focusState.hasFocus) hide() - } - ) { - Row( - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 30.dp), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - ActionButton(Modifier.fillMaxWidth(0.25f), null, stringResource(MR.strings.use_camera_button), icon = painterResource(MR.images.ic_camera_enhance)) { - attachmentOption.value = AttachmentOption.CameraPhoto - hide() - } - ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(MR.strings.gallery_image_button), icon = painterResource(MR.images.ic_add_photo)) { - attachmentOption.value = AttachmentOption.GalleryImage - hide() - } - ActionButton(Modifier.fillMaxWidth(0.50f), null, stringResource(MR.strings.gallery_video_button), icon = painterResource(MR.images.ic_smart_display)) { - attachmentOption.value = AttachmentOption.GalleryVideo - hide() - } - ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(MR.strings.choose_file), icon = painterResource(MR.images.ic_note_add)) { - attachmentOption.value = AttachmentOption.File - hide() - } - } - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt deleted file mode 100644 index b6edae0e8f..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/LocalAuthentication.kt +++ /dev/null @@ -1,148 +0,0 @@ -package chat.simplex.app.views.helpers - -import android.os.Build.VERSION.SDK_INT -import androidx.activity.compose.BackHandler -import androidx.biometric.BiometricManager -import androidx.biometric.BiometricManager.Authenticators.* -import androidx.biometric.BiometricPrompt -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.ui.Modifier -import androidx.core.content.ContextCompat -import androidx.fragment.app.FragmentActivity -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword -import chat.simplex.app.views.localauth.LocalAuthView -import chat.simplex.app.views.usersettings.LAMode -import chat.simplex.res.MR - -sealed class LAResult { - object Success: LAResult() - class Error(val errString: CharSequence): LAResult() - class Failed(val errString: CharSequence? = null): LAResult() - class Unavailable(val errString: CharSequence? = null): LAResult() -} - -data class LocalAuthRequest ( - val title: String?, - val reason: String, - val password: String, - val selfDestruct: Boolean, - val completed: (LAResult) -> Unit -) { - companion object { - val sample = LocalAuthRequest(generalGetString(MR.strings.la_enter_app_passcode), generalGetString(MR.strings.la_authenticate), "", selfDestruct = false) { } - } -} - -fun authenticate( - promptTitle: String, - promptSubtitle: String, - selfDestruct: Boolean = false, - usingLAMode: LAMode = SimplexApp.context.chatModel.controller.appPrefs.laMode.get(), - completed: (LAResult) -> Unit -) { - val activity = SimplexApp.context.mainActivity.get() ?: return completed(LAResult.Error("")) - when (usingLAMode) { - LAMode.SYSTEM -> when { - SDK_INT in 28..29 -> - // KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types - authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL) - SDK_INT > 29 -> - authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) - else -> completed(LAResult.Unavailable()) - } - LAMode.PASSCODE -> { - val password = ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password))) - ModalManager.shared.showPasscodeCustomModal { close -> - BackHandler { - close() - completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled))) - } - Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { - LocalAuthView(SimplexApp.context.chatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && SimplexApp.context.chatModel.controller.appPrefs.selfDestruct.get()) { - close() - completed(it) - }) - } - } - } - } -} - -private fun authenticateWithBiometricManager( - promptTitle: String, - promptSubtitle: String, - activity: FragmentActivity, - completed: (LAResult) -> Unit, - authenticators: Int -) { - val biometricManager = BiometricManager.from(activity) - when (biometricManager.canAuthenticate(authenticators)) { - BiometricManager.BIOMETRIC_SUCCESS -> { - val executor = ContextCompat.getMainExecutor(activity) - val biometricPrompt = BiometricPrompt( - activity, - executor, - object: BiometricPrompt.AuthenticationCallback() { - override fun onAuthenticationError( - errorCode: Int, - errString: CharSequence - ) { - super.onAuthenticationError(errorCode, errString) - completed(LAResult.Error(errString)) - } - - override fun onAuthenticationSucceeded( - result: BiometricPrompt.AuthenticationResult - ) { - super.onAuthenticationSucceeded(result) - completed(LAResult.Success) - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - completed(LAResult.Failed()) - } - } - ) - val promptInfo = BiometricPrompt.PromptInfo.Builder() - .setTitle(promptTitle) - .setSubtitle(promptSubtitle) - .setAllowedAuthenticators(authenticators) - .setConfirmationRequired(false) - .build() - biometricPrompt.authenticate(promptInfo) - } - else -> completed(LAResult.Unavailable()) - } -} - -fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.auth_simplex_lock_turned_on), - generalGetString(MR.strings.auth_you_will_be_required_to_authenticate_when_you_start_or_resume) -) - -fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.lock_not_enabled), - generalGetString(MR.strings.you_can_turn_on_lock) -) - -fun laFailedAlert() { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.la_auth_failed), - text = generalGetString(MR.strings.la_could_not_be_verified) - ) -} - -fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.auth_unavailable), - generalGetString(MR.strings.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled) -) - -fun laUnavailableTurningOffAlert() = AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.auth_unavailable), - generalGetString(MR.strings.auth_device_authentication_is_disabled_turning_off) -) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Share.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Share.kt deleted file mode 100644 index 10ef455a48..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Share.kt +++ /dev/null @@ -1,128 +0,0 @@ -package chat.simplex.app.views.helpers - -import android.Manifest -import android.content.* -import android.net.Uri -import android.provider.MediaStore -import android.util.Log -import android.webkit.MimeTypeMap -import android.widget.Toast -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider -import chat.simplex.app.* -import chat.simplex.app.model.CIFile -import chat.simplex.res.MR -import java.io.BufferedOutputStream -import java.io.File - -fun shareText(text: String) { - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" - } - val shareIntent = Intent.createChooser(sendIntent, null) - // This flag is needed when you start a new activity from non-Activity context - shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - SimplexApp.context.startActivity(shareIntent) -} - -fun shareFile(text: String, filePath: String) { - val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) - val ext = filePath.substringAfterLast(".") - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return - val sendIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND - /*if (text.isNotEmpty()) { - putExtra(Intent.EXTRA_TEXT, text) - }*/ - putExtra(Intent.EXTRA_STREAM, uri) - type = mimeType - } - val shareIntent = Intent.createChooser(sendIntent, null) - // This flag is needed when you start a new activity from non-Activity context - shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - SimplexApp.context.startActivity(shareIntent) -} - -fun copyText(text: String) { - val clipboard = ContextCompat.getSystemService(SimplexApp.context, ClipboardManager::class.java) - clipboard?.setPrimaryClip(ClipData.newPlainText("text", text)) -} - -fun sendEmail(subject: String, body: CharSequence) { - val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) - emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) - emailIntent.putExtra(Intent.EXTRA_TEXT, body) - // This flag is needed when you start a new activity from non-Activity context - emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - try { - SimplexApp.context.startActivity(emailIntent) - } catch (e: ActivityNotFoundException) { - Log.e(TAG, "No activity was found for handling email intent") - } -} - -@Composable -fun rememberSaveFileLauncher(ciFile: CIFile?): ManagedActivityResultLauncher<String, Uri?> = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument(), - onResult = { destination -> - destination?.let { - val cxt = SimplexApp.context - val filePath = getLoadedFilePath(ciFile) - if (filePath != null) { - val contentResolver = cxt.contentResolver - contentResolver.openOutputStream(destination)?.let { stream -> - val outputStream = BufferedOutputStream(stream) - File(filePath).inputStream().use { it.copyTo(outputStream) } - outputStream.close() - Toast.makeText(cxt, generalGetString(MR.strings.file_saved), Toast.LENGTH_SHORT).show() - } - } else { - Toast.makeText(cxt, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show() - } - } - } - ) - -fun imageMimeType(fileName: String): String { - val lowercaseName = fileName.lowercase() - return when { - lowercaseName.endsWith(".png") -> "image/png" - lowercaseName.endsWith(".gif") -> "image/gif" - lowercaseName.endsWith(".webp") -> "image/webp" - lowercaseName.endsWith(".avif") -> "image/avif" - lowercaseName.endsWith(".svg") -> "image/svg+xml" - else -> "image/jpeg" - } -} - -/** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */ -fun saveImage(ciFile: CIFile?) { - val cxt = SimplexApp.context - val filePath = getLoadedFilePath(ciFile) - val fileName = ciFile?.fileName - if (filePath != null && fileName != null) { - val values = ContentValues() - values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) - values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName)) - values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - values.put(MediaStore.MediaColumns.TITLE, fileName) - val uri = cxt.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) - uri?.let { - cxt.contentResolver.openOutputStream(uri)?.let { stream -> - val outputStream = BufferedOutputStream(stream) - File(filePath).inputStream().use { it.copyTo(outputStream) } - outputStream.close() - Toast.makeText(cxt, generalGetString(MR.strings.image_saved), Toast.LENGTH_SHORT).show() - } - } - } else { - Toast.makeText(cxt, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show() - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Util.kt index 3e9926b17b..e69de29bb2 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -1,687 +0,0 @@ -package chat.simplex.app.views.helpers - -import android.app.Activity -import android.app.Application -//import android.app.LocaleManager -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.res.Configuration -import android.content.res.Resources -import android.graphics.* -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.os.* -import android.provider.OpenableColumns -import android.text.Spanned -import android.text.SpannedString -import android.text.style.* -import android.util.Base64 -import android.util.Log -import android.view.View -import android.view.ViewTreeObserver -import android.view.inputmethod.InputMethodManager -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.Saver -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.* -import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.* -import androidx.compose.ui.text.style.BaselineShift -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.unit.* -import androidx.core.content.FileProvider -import androidx.core.graphics.ColorUtils -import androidx.core.text.HtmlCompat -import chat.simplex.app.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.ThemeOverrides -import com.charleskorn.kaml.decodeFromStream -import chat.simplex.res.MR -import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.* -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import org.apache.commons.io.IOUtils -import java.io.* -import java.text.SimpleDateFormat -import java.util.* -import kotlin.math.* - -fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action) - -fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job = - scope.launch { withContext(Dispatchers.Main, action) } - -fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job = - CoroutineScope(Dispatchers.Default).launch(block = action) - -enum class KeyboardState { - Opened, Closed -} - -@Composable -fun getKeyboardState(): State<KeyboardState> { - val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } - val view = LocalView.current - DisposableEffect(view) { - val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - val screenHeight = view.rootView.height - val keypadHeight = screenHeight - rect.bottom - keyboardState.value = if (keypadHeight > screenHeight * 0.15) { - KeyboardState.Opened - } else { - KeyboardState.Closed - } - } - view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) - - onDispose { - view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) - } - } - - return keyboardState -} - -fun hideKeyboard(view: View) = - (SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0) - -// Resource to annotated string from -// https://stackoverflow.com/questions/68549248/android-jetpack-compose-how-to-show-styled-text-from-string-resources -fun generalGetString(id: StringResource): String { - // prefer stringResource in Composable items to retain preview abilities - return id.getString(SimplexApp.context) -} - -@Composable -@ReadOnlyComposable -private fun resources(): Resources { - LocalConfiguration.current - return LocalContext.current.resources -} - -fun Spanned.toHtmlWithoutParagraphs(): String { - return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) - .substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>") -} - -fun Resources.getText(id: StringResource, vararg args: Any): CharSequence { - val escapedArgs = args.map { - if (it is Spanned) it.toHtmlWithoutParagraphs() else it - }.toTypedArray() - val resource = SpannedString(getText(id)) - val htmlResource = resource.toHtmlWithoutParagraphs() - val formattedHtml = String.format(htmlResource, *escapedArgs) - return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) -} - -fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { - return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) -} - -@Composable -fun annotatedStringResource(id: StringResource): AnnotatedString { - val density = LocalDensity.current - return remember(id) { - val text = id.getString(SimplexApp.context) - escapedHtmlToAnnotatedString(text, density) - } -} - -private fun spannableStringToAnnotatedString( - text: CharSequence, - density: Density, -): AnnotatedString { - return if (text is Spanned) { - with(density) { - buildAnnotatedString { - append((text.toString())) - text.getSpans(0, text.length, Any::class.java).forEach { - val start = text.getSpanStart(it) - val end = text.getSpanEnd(it) - when (it) { - is StyleSpan -> when (it.style) { - Typeface.NORMAL -> addStyle( - SpanStyle( - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - ), - start, - end - ) - Typeface.BOLD -> addStyle( - SpanStyle( - fontWeight = FontWeight.Bold, - fontStyle = FontStyle.Normal - ), - start, - end - ) - Typeface.ITALIC -> addStyle( - SpanStyle( - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Italic - ), - start, - end - ) - Typeface.BOLD_ITALIC -> addStyle( - SpanStyle( - fontWeight = FontWeight.Bold, - fontStyle = FontStyle.Italic - ), - start, - end - ) - } - is TypefaceSpan -> addStyle( - SpanStyle( - fontFamily = when (it.family) { - FontFamily.SansSerif.name -> FontFamily.SansSerif - FontFamily.Serif.name -> FontFamily.Serif - FontFamily.Monospace.name -> FontFamily.Monospace - FontFamily.Cursive.name -> FontFamily.Cursive - else -> FontFamily.Default - } - ), - start, - end - ) - is AbsoluteSizeSpan -> addStyle( - SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()), - start, - end - ) - is RelativeSizeSpan -> addStyle( - SpanStyle(fontSize = it.sizeChange.em), - start, - end - ) - is StrikethroughSpan -> addStyle( - SpanStyle(textDecoration = TextDecoration.LineThrough), - start, - end - ) - is UnderlineSpan -> addStyle( - SpanStyle(textDecoration = TextDecoration.Underline), - start, - end - ) - is SuperscriptSpan -> addStyle( - SpanStyle(baselineShift = BaselineShift.Superscript), - start, - end - ) - is SubscriptSpan -> addStyle( - SpanStyle(baselineShift = BaselineShift.Subscript), - start, - end - ) - is ForegroundColorSpan -> addStyle( - SpanStyle(color = Color(it.foregroundColor)), - start, - end - ) - else -> addStyle(SpanStyle(color = Color.White), start, end) - } - } - } - } - } else { - AnnotatedString(text.toString()) - } -} - -// maximum image file size to be auto-accepted -const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB -const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 -const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 -const val MAX_VIDEO_SIZE_AUTO_RCV: Long = 1_047_552 // 1023KB - -const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 300_000 - -const val MAX_FILE_SIZE_SMP: Long = 8000000 - -const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB - -fun getFilesDirectory(): String { - return SimplexApp.context.filesDir.toString() -} - -fun getTempFilesDirectory(): String { - return "${getFilesDirectory()}/temp_files" -} - -fun getAppFilesDirectory(): String { - return "${getFilesDirectory()}/app_files" -} - -fun getAppFilePath(fileName: String): String { - return "${getAppFilesDirectory()}/$fileName" -} - -fun getAppFileUri(fileName: String): Uri { - return Uri.parse("${getAppFilesDirectory()}/$fileName") -} - - -fun getLoadedFilePath(file: CIFile?): String? { - return if (file?.filePath != null && file.loaded) { - val filePath = getAppFilePath(file.filePath) - if (File(filePath).exists()) filePath else null - } else { - null - } -} - -// https://developer.android.com/training/data-storage/shared/documents-files#bitmap -fun getLoadedImage(file: CIFile?): Bitmap? { - val filePath = getLoadedFilePath(file) - return if (filePath != null) { - try { - val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) - val parcelFileDescriptor = SimplexApp.context.contentResolver.openFileDescriptor(uri, "r") - val fileDescriptor = parcelFileDescriptor?.fileDescriptor - val image = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 1000, 1000) - parcelFileDescriptor?.close() - image - } catch (e: Exception) { - null - } - } else { - null - } -} - -// https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap -private fun decodeSampledBitmapFromFileDescriptor(fileDescriptor: FileDescriptor?, reqWidth: Int, reqHeight: Int): Bitmap { - // First decode with inJustDecodeBounds=true to check dimensions - return BitmapFactory.Options().run { - inJustDecodeBounds = true - BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this) - // Calculate inSampleSize - inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) - // Decode bitmap with inSampleSize set - inJustDecodeBounds = false - - BitmapFactory.decodeFileDescriptor(fileDescriptor, null, this) - } -} - -private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { - // Raw height and width of image - val (height: Int, width: Int) = options.run { outHeight to outWidth } - var inSampleSize = 1 - - if (height > reqHeight || width > reqWidth) { - val halfHeight: Int = height / 2 - val halfWidth: Int = width / 2 - // Calculate the largest inSampleSize value that is a power of 2 and keeps both - // height and width larger than the requested height and width. - while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { - inSampleSize *= 2 - } - } - - return inSampleSize -} - -fun getFileName(uri: Uri): String? { - return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - cursor.getString(nameIndex) - } -} - -fun getAppFilePath(uri: Uri): String? { - return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - getAppFilePath(cursor.getString(nameIndex)) - } -} - -fun getFileSize(uri: Uri): Long? { - return SimplexApp.context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - cursor.getLong(sizeIndex) - } -} - -fun getBitmapFromUri(uri: Uri, withAlertOnException: Boolean = true): Bitmap? { - return if (Build.VERSION.SDK_INT >= 28) { - val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri) - try { - ImageDecoder.decodeBitmap(source) - } catch (e: android.graphics.ImageDecoder.DecodeException) { - Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}") - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.image_decoding_exception_title), - text = generalGetString(MR.strings.image_decoding_exception_desc) - ) - } - null - } - } else { - BitmapFactory.decodeFile(getAppFilePath(uri)) - } -} - -fun getDrawableFromUri(uri: Uri, withAlertOnException: Boolean = true): Drawable? { - return if (Build.VERSION.SDK_INT >= 28) { - val source = ImageDecoder.createSource(SimplexApp.context.contentResolver, uri) - try { - ImageDecoder.decodeDrawable(source) - } catch (e: android.graphics.ImageDecoder.DecodeException) { - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.image_decoding_exception_title), - text = generalGetString(MR.strings.image_decoding_exception_desc) - ) - } - Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") - null - } - } else { - Drawable.createFromPath(getAppFilePath(uri)) - } -} - -fun getThemeFromUri(uri: Uri, withAlertOnException: Boolean = true): ThemeOverrides? { - SimplexApp.context.contentResolver.openInputStream(uri).use { - runCatching { - return yaml.decodeFromStream<ThemeOverrides>(it!!) - }.onFailure { - if (withAlertOnException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.import_theme_error), - text = generalGetString(MR.strings.import_theme_error_desc), - ) - } - } - } - return null -} - -fun saveImage(uri: Uri): String? { - val bitmap = getBitmapFromUri(uri) ?: return null - return saveImage(bitmap) -} - -fun saveImage(image: Bitmap): String? { - return try { - val ext = if (image.hasAlpha()) "png" else "jpg" - val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE) - val fileToSave = generateNewFileName("IMG", ext) - val file = File(getAppFilePath(fileToSave)) - val output = FileOutputStream(file) - dataResized.writeTo(output) - output.flush() - output.close() - fileToSave - } catch (e: Exception) { - Log.e(chat.simplex.app.TAG, "Util.kt saveImage error: ${e.message}") - null - } -} - -fun saveAnimImage(uri: Uri): String? { - return try { - val filename = getFileName(uri)?.lowercase() - var ext = when { - // remove everything but extension - filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "") - else -> "gif" - } - // Just in case the image has a strange extension - if (ext.length < 3 || ext.length > 4) ext = "gif" - val fileToSave = generateNewFileName("IMG", ext) - val file = File(getAppFilePath(fileToSave)) - val output = FileOutputStream(file) - SimplexApp.context.contentResolver.openInputStream(uri)!!.use { input -> - output.use { output -> - input.copyTo(output) - } - } - fileToSave - } catch (e: Exception) { - Log.e(chat.simplex.app.TAG, "Util.kt saveAnimImage error: ${e.message}") - null - } -} - -fun saveTempImageUncompressed(image: Bitmap, asPng: Boolean): File? { - return try { - val ext = if (asPng) "png" else "jpg" - val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE) - return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply { - outputStream().use { out -> - image.compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out) - out.flush() - } - deleteOnExit() - SimplexApp.context.chatModel.filesToDelete.add(this) - } - } catch (e: Exception) { - Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}") - null - } -} - -fun saveFileFromUri(uri: Uri): String? { - return try { - val inputStream = SimplexApp.context.contentResolver.openInputStream(uri) - val fileToSave = getFileName(uri) - if (inputStream != null && fileToSave != null) { - val destFileName = uniqueCombine(fileToSave) - val destFile = File(getAppFilePath(destFileName)) - IOUtils.copy(inputStream, FileOutputStream(destFile)) - destFileName - } else { - Log.e(chat.simplex.app.TAG, "Util.kt saveFileFromUri null inputStream") - null - } - } catch (e: Exception) { - Log.e(chat.simplex.app.TAG, "Util.kt saveFileFromUri error: ${e.message}") - null - } -} - -fun generateNewFileName(prefix: String, ext: String): String { - val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) - sdf.timeZone = TimeZone.getTimeZone("GMT") - val timestamp = sdf.format(Date()) - return uniqueCombine("${prefix}_$timestamp.$ext") -} - -fun uniqueCombine(fileName: String): String { - val orig = File(fileName) - val name = orig.nameWithoutExtension - val ext = orig.extension - fun tryCombine(n: Int): String { - val suffix = if (n == 0) "" else "_$n" - val f = "$name$suffix.$ext" - return if (File(getAppFilePath(f)).exists()) tryCombine(n + 1) else f - } - return tryCombine(0) -} - -fun formatBytes(bytes: Long): String { - if (bytes == 0.toLong()) { - return "0 bytes" - } - val bytesDouble = bytes.toDouble() - val k = 1024.toDouble() - val units = arrayOf("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") - val i = floor(log2(bytesDouble) / log2(k)) - val size = bytesDouble / k.pow(i) - val unit = units[i.toInt()] - - return if (i <= 1) { - String.format("%.0f %s", size, unit) - } else { - String.format("%.2f %s", size, unit) - } -} - -fun removeFile(fileName: String): Boolean { - val file = File(getAppFilePath(fileName)) - val fileDeleted = file.delete() - if (!fileDeleted) { - Log.e(chat.simplex.app.TAG, "Util.kt removeFile error") - } - return fileDeleted -} - -fun deleteAppFiles() { - val dir = File(getAppFilesDirectory()) - try { - dir.list()?.forEach { - removeFile(it) - } - } catch (e: java.lang.Exception) { - Log.e(TAG, "Util deleteAppFiles error: ${e.stackTraceToString()}") - } -} - -fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in bytes - var fileCount = 0 - var bytes = 0L - try { - File(dir).listFiles()?.forEach { - fileCount++ - bytes += it.length() - } - } catch (e: java.lang.Exception) { - Log.e(TAG, "Util directoryFileCountAndSize error: ${e.stackTraceToString()}") - } - return fileCount to bytes -} - -fun getMaxFileSize(fileProtocol: FileProtocol): Long { - return when (fileProtocol) { - FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP - FileProtocol.SMP -> MAX_FILE_SIZE_SMP - } -} - -fun getBitmapFromVideo(uri: Uri, timestamp: Long? = null, random: Boolean = true): VideoPlayer.PreviewAndDuration { - val mmr = MediaMetadataRetriever() - mmr.setDataSource(SimplexApp.context, uri) - val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() - val image = when { - timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST) - random -> mmr.frameAtTime - else -> mmr.getFrameAtTime(0) - } - mmr.release() - return VideoPlayer.PreviewAndDuration(image, durationMs, timestamp ?: 0) -} - -fun Color.darker(factor: Float = 0.1f): Color = - Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha) - -fun Color.lighter(factor: Float = 0.1f): Color = - Color(min(red * (1 + factor), 1f), min(green * (1 + factor), 1f), min(blue * (1 + factor), 1f), alpha) - -fun Color.mixWith(color: Color, alpha: Float): Color = - Color(ColorUtils.blendARGB(color.toArgb(), toArgb(), alpha)) - -fun ByteArray.toBase64String() = Base64.encodeToString(this, Base64.DEFAULT) - -fun String.toByteArrayFromBase64() = Base64.decode(this, Base64.DEFAULT) - -val LongRange.Companion.saver - get() = Saver<MutableState<LongRange>, Pair<Long, Long>>( - save = { it.value.first to it.value.last }, - restore = { mutableStateOf(it.first..it.second) } - ) - -/* Make sure that T class has @Serializable annotation */ -inline fun <reified T> serializableSaver(): Saver<T, *> = Saver( - save = { json.encodeToString(it) }, - restore = { json.decodeFromString(it) } - ) - -fun saveAppLocale(pref: SharedPreference<String?>, activity: Activity, languageCode: String? = null) { -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { -// val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java) -// localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return)) -// } else { - pref.set(languageCode) - if (languageCode == null) { - activity.applyLocale(SimplexApp.context.defaultLocale) - } - activity.recreate() -// } -} - -fun Activity.applyAppLocale(pref: SharedPreference<String?>) { -// if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - val lang = pref.get() ?: return - applyLocale(Locale.forLanguageTag(lang)) -// } -} - -private fun Activity.applyLocale(locale: Locale) { - Locale.setDefault(locale) - val appConf = Configuration(SimplexApp.context.resources.configuration).apply { setLocale(locale) } - val activityConf = Configuration(resources.configuration).apply { setLocale(locale) } - @Suppress("DEPRECATION") - SimplexApp.context.resources.updateConfiguration(appConf, resources.displayMetrics) - @Suppress("DEPRECATION") - resources.updateConfiguration(activityConf, resources.displayMetrics) -} - -fun UriHandler.openUriCatching(uri: String) { - try { - openUri(uri) - } catch (e: ActivityNotFoundException) { - Log.e(TAG, e.stackTraceToString()) - } -} - -fun IntSize.Companion.Saver(): Saver<IntSize, *> = Saver( - save = { it.width to it.height }, - restore = { IntSize(it.first, it.second) } -) - -@Composable -fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { - val context = LocalContext.current - DisposableEffect(Unit) { - always() - val activity = context as? Activity ?: return@DisposableEffect onDispose {} - val orientation = activity.resources.configuration.orientation - onDispose { - whenDispose() - if (orientation == activity.resources.configuration.orientation) { - whenGone() - } - } - } -} - -@Composable -fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) { - val context = LocalContext.current - DisposableEffect(Unit) { - always() - val activity = context as? Activity ?: return@DisposableEffect onDispose {} - val orientation = activity.resources.configuration.orientation - onDispose { - whenDispose() - if (orientation != activity.resources.configuration.orientation) { - whenRotate() - } - } - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt deleted file mode 100644 index 39bd6cd333..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ /dev/null @@ -1,171 +0,0 @@ -package chat.simplex.app.views.newchat - -import SectionBottomSpacer -import SectionSpacer -import SectionView -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import dev.icerock.moko.resources.compose.painterResource -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.SettingsActionItem -import chat.simplex.res.MR - -@Composable -fun AddContactView(connReqInvitation: String, connIncognito: Boolean) { - AddContactLayout( - connReq = connReqInvitation, - connIncognito = connIncognito, - share = { shareText(connReqInvitation) }, - learnMore = { - ModalManager.shared.showModal { - Column( - Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween - ) { - AddContactLearnMore() - } - } - } - ) -} - -@Composable -fun AddContactLayout(connReq: String, connIncognito: Boolean, share: () -> Unit, learnMore: () -> Unit) { - Column( - Modifier - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween, - ) { - AppBarTitle(stringResource(MR.strings.add_contact)) - OneTimeLinkProfileText(connIncognito) - - SectionSpacer() - SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) { - OneTimeLinkSection(connReq, share, learnMore) - } - SectionBottomSpacer() - } -} - -@Composable -fun OneTimeLinkProfileText(connIncognito: Boolean) { - Row(Modifier.padding(horizontal = DEFAULT_PADDING)) { - InfoAboutIncognito( - connIncognito, - true, - generalGetString(MR.strings.incognito_random_profile_description), - generalGetString(MR.strings.your_profile_will_be_sent) - ) - } -} - -@Composable -fun ColumnScope.OneTimeLinkSection(connReq: String, share: () -> Unit, learnMore: () -> Unit) { - if (connReq.isNotEmpty()) { - QRCode( - connReq, Modifier - .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) - .aspectRatio(1f) - ) - } else { - CircularProgressIndicator( - Modifier - .size(36.dp) - .padding(4.dp) - .align(Alignment.CenterHorizontally), - color = MaterialTheme.colors.secondary, - strokeWidth = 3.dp - ) - } - ShareLinkButton(share) - OneTimeLinkLearnMoreButton(learnMore) -} - -@Composable -fun ShareLinkButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_share), - stringResource(MR.strings.share_invitation_link), - onClick, - iconColor = MaterialTheme.colors.primary, - textColor = MaterialTheme.colors.primary, - ) -} - -@Composable -fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_info), - stringResource(MR.strings.learn_more), - onClick, - ) -} - -@Composable -fun InfoAboutIncognito(chatModelIncognito: Boolean, supportedIncognito: Boolean = true, onText: String, offText: String, centered: Boolean = false) { - if (chatModelIncognito) { - Row( - Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start - ) { - Icon( - if (supportedIncognito) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_info), - stringResource(MR.strings.incognito), - tint = if (supportedIncognito) Indigo else WarningOrange, - modifier = Modifier.padding(end = 10.dp).size(20.dp) - ) - Text(onText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2) - } - } else { - Row( - Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = if (centered) Arrangement.Center else Arrangement.Start - ) { - Icon( - painterResource(MR.images.ic_info), - stringResource(MR.strings.incognito), - tint = MaterialTheme.colors.secondary, - modifier = Modifier.padding(end = 10.dp).size(20.dp) - ) - Text(offText, textAlign = if (centered) TextAlign.Center else TextAlign.Left, style = MaterialTheme.typography.body2) - } - } -} - -@Preview -@Preview( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -) -@Composable -fun PreviewAddContactView() { - SimpleXTheme { - AddContactLayout( - connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", - connIncognito = false, - share = {}, - learnMore = {}, - ) - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt deleted file mode 100644 index f7f1770abd..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/PasteToConnect.kt +++ /dev/null @@ -1,139 +0,0 @@ -package chat.simplex.app.views.newchat - -import SectionBottomSpacer -import android.content.ClipboardManager -import android.content.res.Configuration -import android.net.Uri -import android.util.Log -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat.getSystemService -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.res.MR - -@Composable -fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { - val connectionLink = remember { mutableStateOf("") } - val context = LocalContext.current - val clipboard = getSystemService(context, ClipboardManager::class.java) - PasteToConnectLayout( - chatModel.incognito.value, - connectionLink = connectionLink, - pasteFromClipboard = { - connectionLink.value = clipboard?.primaryClip?.getItemAt(0)?.coerceToText(context) as? String ?: return@PasteToConnectLayout - }, - connectViaLink = { connReqUri -> - try { - val uri = Uri.parse(connReqUri) - withUriAction(uri) { linkType -> - val action = suspend { - Log.d(TAG, "connectViaUri: connecting") - if (connectViaUri(chatModel, linkType, uri)) { - close() - } - } - if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), - confirmText = generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { action() } } - ) - } else action() - } - } catch (e: RuntimeException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_connection_link), - text = generalGetString(MR.strings.this_string_is_not_a_connection_link) - ) - } - }, - ) -} - -@Composable -fun PasteToConnectLayout( - chatModelIncognito: Boolean, - connectionLink: MutableState<String>, - pasteFromClipboard: () -> Unit, - connectViaLink: (String) -> Unit, -) { - Column( - Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.SpaceBetween, - ) { - AppBarTitle(stringResource(MR.strings.connect_via_link), false) - Text(stringResource(MR.strings.paste_connection_link_below_to_connect)) - - InfoAboutIncognito( - chatModelIncognito, - true, - generalGetString(MR.strings.incognito_random_profile_from_contact_description), - generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) - ) - - Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) { - TextEditor(connectionLink, Modifier.height(180.dp), contentPadding = PaddingValues()) - } - - Row( - Modifier.fillMaxWidth().padding(bottom = 6.dp), - horizontalArrangement = Arrangement.Start, - ) { - if (connectionLink.value == "") { - SimpleButton(text = stringResource(MR.strings.paste_button), icon = painterResource(MR.images.ic_content_paste)) { - pasteFromClipboard() - } - } else { - SimpleButton(text = stringResource(MR.strings.clear_verb), icon = painterResource(MR.images.ic_close)) { - connectionLink.value = "" - } - } - Spacer(Modifier.weight(1f).fillMaxWidth()) - SimpleButton(text = stringResource(MR.strings.connect_button), icon = painterResource(MR.images.ic_link)) { - connectViaLink(connectionLink.value) - } - } - - Text(annotatedStringResource(MR.strings.you_can_also_connect_by_clicking_the_link)) - SectionBottomSpacer() - } -} - - -@Preview(showBackground = true) -@Preview( - uiMode = Configuration.UI_MODE_NIGHT_YES, - name = "Dark Mode" -) -@Composable -fun PreviewPasteToConnectTextbox() { - SimpleXTheme { - PasteToConnectLayout( - chatModelIncognito = false, - connectionLink = remember { mutableStateOf("") }, - pasteFromClipboard = {}, - connectViaLink = { link -> - try { - println(link) - // withApi { chatModel.controller.apiConnect(link) } - } catch (e: Exception) { - e.printStackTrace() - } - }, - ) - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt deleted file mode 100644 index 2bbef58cf8..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ScanToConnectView.kt +++ /dev/null @@ -1,164 +0,0 @@ -package chat.simplex.app.views.newchat - -import SectionBottomSpacer -import android.Manifest -import android.content.res.Configuration -import android.net.Uri -import android.util.Log -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.net.toUri -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.json -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.* -import com.google.accompanist.permissions.rememberPermissionState -import chat.simplex.res.MR -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Composable -fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { - val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) - LaunchedEffect(Unit) { - cameraPermissionState.launchPermissionRequest() - } - ConnectContactLayout( - chatModelIncognito = chatModel.incognito.value, - qrCodeScanner = { - QRCodeScanner { connReqUri -> - try { - val uri = Uri.parse(connReqUri) - withUriAction(uri) { linkType -> - val action = suspend { - Log.d(TAG, "connectViaUri: connecting") - if (connectViaUri(chatModel, linkType, uri)) { - close() - } - } - if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), - confirmText = generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { action() } } - ) - } else action() - } - } catch (e: RuntimeException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_QR_code), - text = generalGetString(MR.strings.this_QR_code_is_not_a_link) - ) - } - } - }, - ) -} - -enum class ConnectionLinkType { - CONTACT, INVITATION, GROUP -} - -@Serializable -sealed class CReqClientData { - @Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData() -} - -fun withUriAction(uri: Uri, run: suspend (ConnectionLinkType) -> Unit) { - val action = uri.path?.drop(1)?.replace("/", "") - val data = uri.toString().replaceFirst("#/", "/").toUri().getQueryParameter("data") - val type = when { - data != null -> { - val parsed = runCatching { - json.decodeFromString(CReqClientData.serializer(), data) - } - when { - parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP - else -> null - } - } - action == "contact" -> ConnectionLinkType.CONTACT - action == "invitation" -> ConnectionLinkType.INVITATION - else -> null - } - if (type != null) { - withApi { run(type) } - } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_contact_link), - text = generalGetString(MR.strings.this_link_is_not_a_valid_connection_link) - ) - } -} - -suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: Uri): Boolean { - val r = chatModel.controller.apiConnect(uri.toString()) - if (r) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.connection_request_sent), - text = - when (action) { - ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted) - ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online) - ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online) - } - ) - } - return r -} - -@Composable -fun ConnectContactLayout(chatModelIncognito: Boolean, qrCodeScanner: @Composable () -> Unit) { - Column( - Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - AppBarTitle(stringResource(MR.strings.scan_QR_code), false) - InfoAboutIncognito( - chatModelIncognito, - true, - generalGetString(MR.strings.incognito_random_profile_description), - generalGetString(MR.strings.your_profile_will_be_sent) - ) - Box( - Modifier - .fillMaxWidth() - .aspectRatio(ratio = 1F) - .padding(bottom = 12.dp) - ) { qrCodeScanner() } - Text( - annotatedStringResource(MR.strings.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link), - lineHeight = 22.sp - ) - SectionBottomSpacer() - } -} - -@Preview -@Preview( - uiMode = Configuration.UI_MODE_NIGHT_YES, - showBackground = true, - name = "Dark Mode" -) -@Composable -fun PreviewConnectContactLayout() { - SimpleXTheme { - ConnectContactLayout( - chatModelIncognito = false, - qrCodeScanner = { Surface {} }, - ) - } -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt deleted file mode 100644 index cdf1f9900a..0000000000 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Appearance.kt +++ /dev/null @@ -1,424 +0,0 @@ -package chat.simplex.app.views.usersettings - -import SectionBottomSpacer -import SectionDividerSpaced -import SectionItemView -import SectionItemViewSpaceBetween -import SectionSpacer -import SectionView -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED -import android.net.Uri -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.material.* -import androidx.compose.material.MaterialTheme.colors -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.* -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import com.godaddy.android.colorpicker.* -import chat.simplex.res.MR -import kotlinx.coroutines.delay -import kotlinx.serialization.encodeToString -import java.io.BufferedOutputStream -import java.util.* -import kotlin.collections.ArrayList - -enum class AppIcon(val resId: Int) { - DEFAULT(R.mipmap.icon), - DARK_BLUE(R.mipmap.icon_dark_blue), -} - -@Composable -fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { - val appIcon = remember { mutableStateOf(findEnabledIcon()) } - - fun setAppIcon(newIcon: AppIcon) { - if (appIcon.value == newIcon) return - val newComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}") - val oldComponent = ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}") - SimplexApp.context.packageManager.setComponentEnabledSetting( - newComponent, - COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP - ) - - SimplexApp.context.packageManager.setComponentEnabledSetting( - oldComponent, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP - ) - - appIcon.value = newIcon - } - - AppearanceLayout( - appIcon, - m.controller.appPrefs.appLanguage, - m.controller.appPrefs.systemDarkTheme, - changeIcon = ::setAppIcon, - showSettingsModal = showSettingsModal, - editColor = { name, initialColor -> - ModalManager.shared.showModalCloseable { close -> - ColorEditor(name, initialColor, close) - } - }, - ) -} - -@Composable fun AppearanceLayout( - icon: MutableState<AppIcon>, - languagePref: SharedPreference<String?>, - systemDarkTheme: SharedPreference<String?>, - changeIcon: (AppIcon) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit, -) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { - AppBarTitle(stringResource(MR.strings.appearance_settings)) - SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { - val context = LocalContext.current -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { -// SectionItemWithValue( -// generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, -// remember { mutableStateOf("system") }, -// listOf(ValueTitleDesc("system", generalGetString(MR.strings.change_verb), "")), -// onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) } -// ) -// } else { - val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } - LangSelector(state) { - state.value = it - withApi { - delay(200) - val activity = context as? Activity - if (activity != null) { - if (it == "system") { - saveAppLocale(languagePref, activity) - } else { - saveAppLocale(languagePref, activity, it) - } - } - } - } -// } - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { - LazyRow { - items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index -> - val item = AppIcon.values()[index] - val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!! - Image( - bitmap = mipmap.toBitmap().asImageBitmap(), - contentDescription = "", - contentScale = ContentScale.Fit, - modifier = Modifier - .shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondaryVariant) - .size(70.dp) - .clickable { changeIcon(item) } - .padding(10.dp) - ) - - if (index + 1 != AppIcon.values().size) { - Spacer(Modifier.padding(horizontal = 4.dp)) - } - } - } - } - - SectionDividerSpaced(maxTopPadding = true) - val currentTheme by CurrentColors.collectAsState() - SectionView(stringResource(MR.strings.settings_section_title_themes)) { - val darkTheme = isSystemInDarkTheme() - val state = remember { derivedStateOf { currentTheme.name } } - ThemeSelector(state) { - ThemeManager.applyTheme(it, darkTheme) - } - if (state.value == DefaultTheme.SYSTEM.name) { - DarkThemeSelector(remember { systemDarkTheme.state }) { - ThemeManager.changeDarkTheme(it, darkTheme) - } - } - } - SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) } - SectionBottomSpacer() - } -} - -@Composable -fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { - Column( - Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), - ) { - val currentTheme by CurrentColors.collectAsState() - - AppBarTitle(stringResource(MR.strings.customize_theme_title)) - - SectionView(stringResource(MR.strings.theme_colors_section_title)) { - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { - val title = generalGetString(MR.strings.color_primary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { - val title = generalGetString(MR.strings.color_primary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { - val title = generalGetString(MR.strings.color_secondary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { - val title = generalGetString(MR.strings.color_secondary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { - val title = generalGetString(MR.strings.color_background) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.background) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { - val title = generalGetString(MR.strings.color_surface) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.surface) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { - val title = generalGetString(MR.strings.color_title) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { - val title = generalGetString(MR.strings.color_sent_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { - val title = generalGetString(MR.strings.color_received_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) - } - } - val isInDarkTheme = isInDarkTheme() - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { - SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { - Text(generalGetString(MR.strings.reset_color), color = colors.primary) - } - } - SectionSpacer() - SectionView { - val theme = remember { mutableStateOf(null as String?) } - val exportThemeLauncher = rememberSaveThemeLauncher(theme) - SectionItemView({ - val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) - theme.value = yaml.encodeToString<ThemeOverrides>(overrides) - exportThemeLauncher.launch("simplex.theme") - }) { - Text(generalGetString(MR.strings.export_theme), color = colors.primary) - } - - val importThemeLauncher = rememberGetContentLauncher { uri: Uri? -> - if (uri != null) { - val theme = getThemeFromUri(uri) - if (theme != null) { - ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) - } - } - } - // Can not limit to YAML mime type since it's unsupported by Android - SectionItemView({ importThemeLauncher.launch("*/*") }) { - Text(generalGetString(MR.strings.import_theme), color = colors.primary) - } - } - SectionBottomSpacer() - } -} - -@Composable -fun ColorEditor( - name: ThemeColor, - initialColor: Color, - close: () -> Unit, -) { - Column( - Modifier - .fillMaxWidth() - ) { - AppBarTitle(name.text) - var currentColor by remember { mutableStateOf(initialColor) } - ColorPicker(initialColor) { - currentColor = it - } - - SectionSpacer() - val isInDarkTheme = isInDarkTheme() - TextButton( - onClick = { - ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) - close() - }, - Modifier.align(Alignment.CenterHorizontally), - colors = ButtonDefaults.textButtonColors(contentColor = currentColor) - ) { - Text(generalGetString(MR.strings.save_color)) - } - } -} - -@Composable -fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { - ClassicColorPicker( - color = initialColor, - modifier = Modifier - .fillMaxWidth() - .height(300.dp), - showAlphaBar = true, - onColorChanged = { color: HsvColor -> - onColorChanged(color.toColor()) - } - ) -} - -@Composable -private fun LangSelector(state: State<String>, onSelected: (String) -> Unit) { - // Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs` - val supportedLanguages = mapOf( - "system" to generalGetString(MR.strings.language_system), - "en" to "English", - "cs" to "Čeština", - "de" to "Deutsch", - "es" to "Español", - "fr" to "Français", - "it" to "Italiano", - "ja" to "日本語", - "nl" to "Nederlands", - "pl" to "Polski", - "pt-BR" to "Português (Brasil)", - "ru" to "Русский", - "zh-CN" to "简体中文" - ) - val values by remember { mutableStateOf(supportedLanguages.map { it.key to it.value }) } - ExposedDropDownSettingRow( - generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, - values, - state, - icon = null, - enabled = remember { mutableStateOf(true) }, - onSelected = onSelected - ) -} - -@Composable -private fun ThemeSelector(state: State<String>, onSelected: (String) -> Unit) { - val darkTheme = isSystemInDarkTheme() - val values by remember { mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) } - ExposedDropDownSettingRow( - generalGetString(MR.strings.theme), - values, - state, - icon = null, - enabled = remember { mutableStateOf(true) }, - onSelected = onSelected - ) -} - -@Composable -private fun DarkThemeSelector(state: State<String?>, onSelected: (String) -> Unit) { - val values by remember { - val darkThemes = ArrayList<Pair<String, String>>() - darkThemes.add(DefaultTheme.DARK.name to generalGetString(MR.strings.theme_dark)) - darkThemes.add(DefaultTheme.SIMPLEX.name to generalGetString(MR.strings.theme_simplex)) - mutableStateOf(darkThemes.toList()) - } - ExposedDropDownSettingRow( - generalGetString(MR.strings.dark_theme), - values, - state, - icon = null, - enabled = remember { mutableStateOf(true) }, - onSelected = { if (it != null) onSelected(it) } - ) -} - -//private fun openSystemLangPicker(activity: Activity) { -// activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName))) -//} - -@Composable -private fun rememberSaveThemeLauncher(theme: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument(), - onResult = { destination -> - val cxt = SimplexApp.context - try { - destination?.let { - val theme = theme.value - if (theme != null) { - val contentResolver = cxt.contentResolver - contentResolver.openOutputStream(destination)?.let { stream -> - BufferedOutputStream(stream).use { outputStream -> - theme.byteInputStream().use { it.copyTo(outputStream) } - } - Toast.makeText(cxt, generalGetString(MR.strings.file_saved), Toast.LENGTH_SHORT).show() - } - } - } - } catch (e: Error) { - Toast.makeText(cxt, generalGetString(MR.strings.error_saving_file), Toast.LENGTH_SHORT).show() - Log.e(TAG, "rememberSaveThemeLauncher error saving theme $e") - } finally { - theme.value = null - } - } - ) - -private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> - SimplexApp.context.packageManager.getComponentEnabledSetting( - ComponentName(BuildConfig.APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") - ).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED } -} - -@Preview(showBackground = true) -@Composable -fun PreviewAppearanceSettings() { - SimpleXTheme { - AppearanceLayout( - icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, - languagePref = SharedPreference({ null }, {}), - systemDarkTheme = SharedPreference({ null }, {}), - changeIcon = {}, - showSettingsModal = { {} }, - editColor = { _, _ -> }, - ) - } -} diff --git a/apps/multiplatform/build.gradle.kts b/apps/multiplatform/build.gradle.kts index 94cecd1725..3a6fbcbf94 100644 --- a/apps/multiplatform/build.gradle.kts +++ b/apps/multiplatform/build.gradle.kts @@ -1,6 +1,5 @@ -import org.gradle.initialization.Environment.Properties import java.io.File -import java.io.FileInputStream +import java.util.* buildscript { val prop = java.util.Properties().apply { @@ -10,6 +9,8 @@ buildscript { // No file was created } } + fun ExtraPropertiesExtension.getOrNull(name: String): Any? = if (has(name)) get("name") else null + extra.set("compose.version", prop["compose.version"] ?: extra["compose.version"]) extra.set("kotlin.version", prop["kotlin.version"] ?: extra["kotlin.version"]) extra.set("gradle.plugin.version", prop["gradle.plugin.version"] ?: extra["gradle.plugin.version"]) @@ -26,6 +27,17 @@ buildscript { extra.set("compression.level", (prop["compression.level"] as String?)?.toIntOrNull() ?: 0) // NOTE: If you need a different version of something, provide it in `local.properties` // like so: compose.version=123, or gradle.plugin.version=1.2.3, etc + + + /** Mac signing and notarization */ + // You can specify `compose.desktop.mac.*` keys and values from the right side of the command in `$HOME/.gradle/gradle.properties`. + // This will be project-independent setup without requiring to have `local.properties` file + extra.set("desktop.mac.signing.identity", prop["desktop.mac.signing.identity"] ?: extra.getOrNull("compose.desktop.mac.signing.identity")) + extra.set("desktop.mac.signing.keychain", prop["desktop.mac.signing.keychain"] ?: extra.getOrNull("compose.desktop.mac.signing.keychain")) + extra.set("desktop.mac.notarization.apple_id", prop["desktop.mac.notarization.apple_id"] ?: extra.getOrNull("compose.desktop.mac.notarization.appleID")) + extra.set("desktop.mac.notarization.password", prop["desktop.mac.notarization.password"] ?: extra.getOrNull("compose.desktop.mac.notarization.password")) + extra.set("desktop.mac.notarization.team_id", prop["desktop.mac.notarization.team_id"] ?: extra.getOrNull("compose.desktop.mac.notarization.ascProvider")) + repositories { google() mavenCentral() @@ -34,7 +46,7 @@ buildscript { classpath("com.android.tools.build:gradle:${rootProject.extra["gradle.plugin.version"]}") classpath(kotlin("gradle-plugin", version = rootProject.extra["kotlin.version"] as String)) classpath("org.jetbrains.kotlin:kotlin-serialization:1.3.2") - classpath("dev.icerock.moko:resources-generator:0.22.3") + classpath("dev.icerock.moko:resources-generator:0.23.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -55,6 +67,7 @@ allprojects { google() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://oss.sonatype.org/content/repositories/snapshots") maven("https://jitpack.io") } } diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index f591378a25..6a5fd1d0fe 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -31,7 +31,6 @@ kotlin { } val commonMain by getting { - kotlin.srcDir("./build/generated/moko/commonMain/src/") dependencies { api(compose.runtime) api(compose.foundation) @@ -40,7 +39,7 @@ kotlin { api("org.jetbrains.kotlinx:kotlinx-datetime:0.3.2") api("com.russhwolf:multiplatform-settings:1.0.0") api("com.charleskorn.kaml:kaml:0.43.0") - api("dev.icerock.moko:resources-compose:0.22.3") + api("dev.icerock.moko:resources-compose:0.23.0") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") //Barcode @@ -49,7 +48,7 @@ kotlin { // Link Previews implementation("org.jsoup:jsoup:1.13.1") // Resources - implementation("dev.icerock.moko:resources:0.22.3") + implementation("dev.icerock.moko:resources:0.23.0") } } val commonTest by getting { @@ -57,54 +56,48 @@ kotlin { implementation(kotlin("test")) } } - // LALAL CHANGE TO IMPLEMENTATION val androidMain by getting { - kotlin.srcDir("./build/generated/moko/commonMain/src/") dependencies { - api("androidx.appcompat:appcompat:1.5.1") - api("androidx.core:core-ktx:1.9.0") - api("androidx.activity:activity-compose:1.5.0") + implementation("androidx.activity:activity-compose:1.5.0") val work_version = "2.7.1" - api("androidx.work:work-runtime-ktx:$work_version") - api("androidx.work:work-multiprocess:$work_version") - api("com.google.accompanist:accompanist-insets:0.23.0") - api("dev.icerock.moko:resources:0.22.3") + implementation("androidx.work:work-runtime-ktx:$work_version") + implementation("com.google.accompanist:accompanist-insets:0.23.0") + implementation("dev.icerock.moko:resources:0.23.0") // Video support - api("com.google.android.exoplayer:exoplayer:2.17.1") + implementation("com.google.android.exoplayer:exoplayer:2.17.1") // Biometric authentication - api("androidx.biometric:biometric:1.2.0-alpha04") + implementation("androidx.biometric:biometric:1.2.0-alpha04") //Barcode - api("org.boofcv:boofcv-android:0.40.1") + implementation("org.boofcv:boofcv-android:0.40.1") //Camera Permission - api("com.google.accompanist:accompanist-permissions:0.23.0") + implementation("com.google.accompanist:accompanist-permissions:0.23.0") - api("androidx.webkit:webkit:1.4.0") + implementation("androidx.webkit:webkit:1.4.0") // GIFs support - api("io.coil-kt:coil-compose:2.1.0") - api("io.coil-kt:coil-gif:2.1.0") + implementation("io.coil-kt:coil-compose:2.1.0") + implementation("io.coil-kt:coil-gif:2.1.0") - api("com.jakewharton:process-phoenix:2.1.2") + implementation("com.jakewharton:process-phoenix:2.1.2") val camerax_version = "1.1.0-beta01" - api("androidx.camera:camera-core:${camerax_version}") - api("androidx.camera:camera-camera2:${camerax_version}") - api("androidx.camera:camera-lifecycle:${camerax_version}") - api("androidx.camera:camera-view:${camerax_version}") - - // LALAL REMOVE - api("org.jsoup:jsoup:1.13.1") - api("com.godaddy.android.colorpicker:compose-color-picker-jvm:0.7.0") - api("androidx.compose.ui:ui-tooling-preview:${extra["compose.version"]}") + implementation("androidx.camera:camera-core:${camerax_version}") + implementation("androidx.camera:camera-camera2:${camerax_version}") + implementation("androidx.camera:camera-lifecycle:${camerax_version}") + implementation("androidx.camera:camera-view:${camerax_version}") } } val desktopMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.1") + implementation("com.github.Dansoftowner:jSystemThemeDetector:3.6") + implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT") + implementation("org.slf4j:slf4j-simple:2.0.7") + implementation("uk.co.caprica:vlcj:4.7.0") } } val desktopTest by getting @@ -116,12 +109,21 @@ android { sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") defaultConfig { minSdkVersion(26) - targetSdkVersion(32) + targetSdkVersion(33) } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } + val isAndroid = gradle.startParameter.taskNames.find { + val lower = it.toLowerCase() + lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install") + } != null + if (isAndroid) { + // This is not needed on Android but can't be moved to desktopMain because MR lib don't support this. + // No other ways to exclude a file work but it's large and should be excluded + kotlin.sourceSets["commonMain"].resources.exclude("/MR/fonts/NotoColorEmoji-Regular.ttf") + } } multiplatformResources { @@ -136,3 +138,97 @@ buildConfig { buildConfigField("String", "DESKTOP_VERSION_NAME", "\"${extra["desktop.version_name"]}\"") } } + +afterEvaluate { + tasks.named("generateMRcommonMain") { + dependsOn("adjustFormatting") + } + tasks.create("adjustFormatting") { + doLast { + val debug = false + val stringRegex = Regex(".*<string .*</string>.*") + val startStringRegex = Regex("<string [^>]*>") + val endStringRegex = Regex("</string>[ ]*") + val endTagRegex = Regex("</") + val anyHtmlRegex = Regex("[^>]*>.*(<|>).*</string>|[^>]*>.*(<|>).*</string>") + val correctHtmlRegex = Regex("[^>]*>.*<b>.*</b>.*</string>|[^>]*>.*<i>.*</i>.*</string>|[^>]*>.*<u>.*</u>.*</string>|[^>]*>.*<font[^>]*>.*</font>.*</string>") + + fun String.removeCDATA(): String = + if (contains("<![CDATA")) { + replace("<![CDATA[", "").replace("]]></string>", "</string>") + } else { + this + } + + fun String.addCDATA(filepath: String): String { + //return this + if (anyHtmlRegex.matches(this)) { + val countOfStartTag = count { it == '<' } + val countOfEndTag = count { it == '>' } + if (countOfStartTag != countOfEndTag || countOfStartTag != endTagRegex.findAll(this).count() * 2 || !correctHtmlRegex.matches(this)) { + if (debug) { + println("Wrong string:") + println(this) + println("in $filepath") + println(" ") + } else { + throw Exception("Wrong string: $this \nin $filepath") + } + } + val res = replace(startStringRegex) { it.value + "<![CDATA[" }.replace(endStringRegex) { "]]>" + it.value } + if (debug) { + println("Changed string:") + println(this) + println(res) + println(" ") + } + return res + } + if (debug) { + println("Correct string:") + println(this) + println(" ") + } + return this + } + val fileRegex = Regex("MR/../strings.xml$|MR/..-.../strings.xml$|MR/..-../strings.xml$|MR/base/strings.xml$") + kotlin.sourceSets["commonMain"].resources.filter { fileRegex.containsMatchIn(it.absolutePath) }.asFileTree.forEach { file -> + val initialLines = ArrayList<String>() + val finalLines = ArrayList<String>() + file.useLines { lines -> + val multiline = ArrayList<String>() + lines.forEach { line -> + initialLines.add(line) + if (stringRegex.matches(line)) { + finalLines.add(line.removeCDATA().addCDATA(file.absolutePath)) + } else if (multiline.isEmpty() && startStringRegex.containsMatchIn(line)) { + multiline.add(line) + } else if (multiline.isNotEmpty() && endStringRegex.containsMatchIn(line)) { + multiline.add(line) + finalLines.addAll(multiline.joinToString("\n").removeCDATA().addCDATA(file.absolutePath).split("\n")) + multiline.clear() + } else if (multiline.isNotEmpty()) { + multiline.add(line) + } else { + finalLines.add(line) + } + } + if (multiline.isNotEmpty()) { + throw Exception("Unclosed string tag: ${multiline.joinToString("\n")} \nin ${file.absolutePath}") + } + } + + if (!debug && finalLines != initialLines) { + file.writer().use { + finalLines.forEachIndexed { index, line -> + it.write(line) + if (index != finalLines.lastIndex) { + it.write("\n") + } + } + } + } + } + } + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt new file mode 100644 index 0000000000..e237272eb0 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt @@ -0,0 +1,22 @@ +package chat.simplex.common.helpers + +import android.net.Uri +import android.os.Build +import chat.simplex.common.model.NotificationsMode +import java.net.URI + +val NotificationsMode.requiresIgnoringBatterySinceSdk: Int get() = when(this) { + NotificationsMode.OFF -> Int.MAX_VALUE + NotificationsMode.PERIODIC -> Build.VERSION_CODES.M + NotificationsMode.SERVICE -> Build.VERSION_CODES.S + /*INSTANT -> Int.MAX_VALUE - for Firebase notifications */ +} + +val NotificationsMode.requiresIgnoringBattery + get() = requiresIgnoringBatterySinceSdk <= Build.VERSION.SDK_INT + +lateinit var APPLICATION_ID: String + +fun Uri.toURI(): URI = URI(toString()) + +fun URI.toUri(): Uri = Uri.parse(toString()) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt new file mode 100644 index 0000000000..b9d7d27ba9 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Locale.kt @@ -0,0 +1,38 @@ +package chat.simplex.common.helpers + +import android.app.Activity +import android.content.res.Configuration +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.platform.defaultLocale +import java.util.* + +fun Activity.saveAppLocale(pref: SharedPreference<String?>, languageCode: String? = null) { + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // val localeManager = SimplexApp.context.getSystemService(LocaleManager::class.java) + // localeManager.applicationLocales = LocaleList(Locale.forLanguageTag(languageCode ?: return)) + // } else { + pref.set(languageCode) + if (languageCode == null) { + applyLocale(defaultLocale) + } + recreate() + // } +} + +fun Activity.applyAppLocale(pref: SharedPreference<String?>) { + // if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + val lang = pref.get() ?: return + applyLocale(Locale.forLanguageTag(lang)) + // } +} + +private fun Activity.applyLocale(locale: Locale) { + Locale.setDefault(locale) + val appConf = Configuration(androidAppContext.resources.configuration).apply { setLocale(locale) } + val activityConf = Configuration(resources.configuration).apply { setLocale(locale) } + @Suppress("DEPRECATION") + androidAppContext.resources.updateConfiguration(appConf, resources.displayMetrics) + @Suppress("DEPRECATION") + resources.updateConfiguration(activityConf, resources.displayMetrics) +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt similarity index 60% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt index 314032984a..25369ffdfd 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/SoundPlayer.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/SoundPlayer.kt @@ -1,22 +1,22 @@ -package chat.simplex.app.views.call +package chat.simplex.common.helpers -import android.content.Context import android.media.* import android.net.Uri import android.os.VibrationEffect import android.os.Vibrator import androidx.core.content.ContextCompat -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.views.helpers.withScope +import chat.simplex.common.R +import chat.simplex.common.platform.SoundPlayerInterface +import chat.simplex.common.platform.androidAppContext +import chat.simplex.common.views.helpers.withScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -class SoundPlayer { +object SoundPlayer: SoundPlayerInterface { private var player: MediaPlayer? = null var playing = false - fun start(scope: CoroutineScope, sound: Boolean) { + override fun start(scope: CoroutineScope, sound: Boolean) { player?.reset() player = MediaPlayer().apply { setAudioAttributes( @@ -25,10 +25,10 @@ class SoundPlayer { .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE) .build() ) - setDataSource(SimplexApp.context, Uri.parse("android.resource://" + SimplexApp.context.packageName + "/" + R.raw.ring_once)) + setDataSource(androidAppContext, Uri.parse("android.resource://" + androidAppContext.packageName + "/" + R.raw.ring_once)) prepare() } - val vibrator = ContextCompat.getSystemService(SimplexApp.context, Vibrator::class.java) + val vibrator = ContextCompat.getSystemService(androidAppContext, Vibrator::class.java) val effect = VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE) playing = true withScope(scope) { @@ -40,12 +40,8 @@ class SoundPlayer { } } - fun stop() { + override fun stop() { playing = false player?.stop() } - - companion object { - val shared = SoundPlayer() - } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt new file mode 100644 index 0000000000..192f3dcc29 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt @@ -0,0 +1,70 @@ +package chat.simplex.common.platform + +import android.annotation.SuppressLint +import android.content.Context +import android.net.LocalServerSocket +import android.util.Log +import androidx.fragment.app.FragmentActivity +import chat.simplex.common.* +import chat.simplex.common.platform.* +import java.io.* +import java.lang.ref.WeakReference +import java.util.* +import java.util.concurrent.Semaphore +import kotlin.concurrent.thread +import kotlin.random.Random + +actual val appPlatform = AppPlatform.ANDROID + +var isAppOnForeground: Boolean = false + +@Suppress("ConstantLocale") +val defaultLocale: Locale = Locale.getDefault() + +@SuppressLint("StaticFieldLeak") +lateinit var androidAppContext: Context +lateinit var mainActivity: WeakReference<FragmentActivity> + +fun initHaskell() { + val socketName = "chat.simplex.app.local.socket.address.listen.native.cmd2" + Random.nextLong(100000) + val s = Semaphore(0) + thread(name="stdout/stderr pipe") { + Log.d(TAG, "starting server") + var server: LocalServerSocket? = null + for (i in 0..100) { + try { + server = LocalServerSocket(socketName + i) + break + } catch (e: IOException) { + Log.e(TAG, e.stackTraceToString()) + } + } + if (server == null) { + throw Error("Unable to setup local server socket. Contact developers") + } + Log.d(TAG, "started server") + s.release() + val receiver = server.accept() + Log.d(TAG, "started receiver") + val logbuffer = FifoQueue<String>(500) + if (receiver != null) { + val inStream = receiver.inputStream + val inStreamReader = InputStreamReader(inStream) + val input = BufferedReader(inStreamReader) + Log.d(TAG, "starting receiver loop") + while (true) { + val line = input.readLine() ?: break + Log.w("$TAG (stdout/stderr)", line) + logbuffer.add(line) + } + Log.w(TAG, "exited receiver loop") + } + } + + System.loadLibrary("app-lib") + + s.acquire() + pipeStdOutToSocket(socketName) + + initHS() +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Back.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Back.android.kt new file mode 100644 index 0000000000..dfaeac695e --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Back.android.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* + +@SuppressWarnings("MissingJvmstatic") +@Composable +actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { + androidx.activity.compose.BackHandler(enabled, onBack) +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt similarity index 83% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt index 1099b2fb2d..dc6c53ecbc 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Cryptor.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt @@ -1,24 +1,23 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.platform import android.annotation.SuppressLint import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties -import android.util.Log -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.views.helpers.AlertManager -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import java.security.KeyStore import javax.crypto.* import javax.crypto.spec.GCMParameterSpec +actual val cryptor: CryptorInterface = Cryptor() + @SuppressLint("ObsoleteSdkInt") -internal class Cryptor { +internal class Cryptor: CryptorInterface { private var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } private var warningShown = false - fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? { + override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? { val secretKey = getSecretKey(alias) if (secretKey == null) { if (!warningShown) { @@ -37,13 +36,13 @@ internal class Cryptor { return runCatching { String(cipher.doFinal(data))}.onFailure { Log.e(TAG, "doFinal: ${it.stackTraceToString()}") }.getOrNull() } - fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> { + override fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> { val cipher: Cipher = Cipher.getInstance(TRANSFORMATION) cipher.init(Cipher.ENCRYPT_MODE, createSecretKey(alias)) return Pair(cipher.doFinal(text.toByteArray(charset("UTF-8"))), cipher.iv) } - fun deleteKey(alias: String) { + override fun deleteKey(alias: String) { if (!keyStore.containsAlias(alias)) return keyStore.deleteEntry(alias) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt new file mode 100644 index 0000000000..161bc51e61 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt @@ -0,0 +1,71 @@ +package chat.simplex.common.platform + +import android.app.Application +import android.net.Uri +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import chat.simplex.common.helpers.toURI +import chat.simplex.common.helpers.toUri +import java.io.* +import java.net.URI + +actual val dataDir: File = androidAppContext.dataDir +actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE) +actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files") +actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files") +actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files") +actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files" + +actual val chatDatabaseFileName: String = "files_chat.db" +actual val agentDatabaseFileName: String = "files_agent.db" + +actual val databaseExportDir: File = androidAppContext.cacheDir + +actual fun desktopOpenDatabaseDir() {} + +@Composable +actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher { + val launcher = rememberLauncherForActivityResult( + contract = if (getContent) ActivityResultContracts.GetContent() else ActivityResultContracts.CreateDocument(), + onResult = { onResult(it?.toURI()) } + ) + return FileChooserLauncher(launcher) +} + +@Composable +actual fun rememberFileChooserMultipleLauncher(onResult: (List<URI>) -> Unit): FileChooserMultipleLauncher { + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetMultipleContents(), + onResult = { onResult(it.map { it.toURI() }) } + ) + return FileChooserMultipleLauncher(launcher) +} + +actual class FileChooserLauncher actual constructor() { + private lateinit var launcher: ManagedActivityResultLauncher<String, Uri?> + + constructor(launcher: ManagedActivityResultLauncher<String, Uri?>): this() { + this.launcher = launcher + } + + actual suspend fun launch(input: String) { + launcher.launch(input) + } +} + +actual class FileChooserMultipleLauncher actual constructor() { + private lateinit var launcher: ManagedActivityResultLauncher<String, List<Uri>> + + constructor(launcher: ManagedActivityResultLauncher<String, List<Uri>>): this() { + this.launcher = launcher + } + + actual suspend fun launch(input: String) { + launcher.launch(input) + } +} + +actual fun URI.inputStream(): InputStream? = androidAppContext.contentResolver.openInputStream(toUri()) +actual fun URI.outputStream(): OutputStream = androidAppContext.contentResolver.openOutputStream(toUri())!! diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt new file mode 100644 index 0000000000..832f0d9cbb --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Images.android.kt @@ -0,0 +1,118 @@ +package chat.simplex.common.platform + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.drawable.AnimatedImageDrawable +import android.os.Build +import android.util.Base64 +import android.webkit.MimeTypeMap +import androidx.compose.ui.graphics.* +import androidx.core.graphics.applyCanvas +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.scale +import boofcv.android.ConvertBitmap +import boofcv.struct.image.GrayU8 +import chat.simplex.common.R +import chat.simplex.common.views.helpers.errorBitmap +import chat.simplex.common.views.helpers.getFileName +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.URI +import kotlin.math.min +import kotlin.math.sqrt + +actual fun base64ToBitmap(base64ImageString: String): ImageBitmap { + val imageString = base64ImageString + .removePrefix("data:image/png;base64,") + .removePrefix("data:image/jpg;base64,") + return try { + val imageBytes = Base64.decode(imageString, Base64.NO_WRAP) + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size).asImageBitmap() + } catch (e: Exception) { + Log.e(TAG, "base64ToBitmap error: $e") + errorBitmap.asImageBitmap() + } +} + +actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String { + var img = image + var str = compressImageStr(img) + while (str.length > maxDataSize) { + val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble()) + val clippedRatio = min(ratio, 2.0) + val width = (img.width.toDouble() / clippedRatio).toInt() + val height = img.height * width / img.width + img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap() + str = compressImageStr(img) + } + return str +} + +// Inspired by https://github.com/MakeItEasyDev/Jetpack-Compose-Capture-Image-Or-Choose-from-Gallery +actual fun cropToSquare(image: ImageBitmap): ImageBitmap { + var xOffset = 0 + var yOffset = 0 + val side = min(image.height, image.width) + if (image.height < image.width) { + xOffset = (image.width - side) / 2 + } else { + yOffset = (image.height - side) / 2 + } + return Bitmap.createBitmap(image.asAndroidBitmap(), xOffset, yOffset, side, side).asImageBitmap() +} + +actual fun compressImageStr(bitmap: ImageBitmap): String { + val usePng = bitmap.hasAlpha() + val ext = if (usePng) "png" else "jpg" + return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP) +} + +actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream { + val stream = ByteArrayOutputStream() + bitmap.asAndroidBitmap().compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream) + return stream +} + +actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream { + var img = image + var stream = compressImageData(img, usePng) + while (stream.size() > maxDataSize) { + val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble()) + val clippedRatio = min(ratio, 2.0) + val width = (img.width.toDouble() / clippedRatio).toInt() + val height = img.height * width / img.width + img = Bitmap.createScaledBitmap(img.asAndroidBitmap(), width, height, true).asImageBitmap() + stream = compressImageData(img, usePng) + } + return stream +} + +actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBitmap.grayToBitmap(this, Bitmap.Config.RGB_565).asImageBitmap() + +actual fun ImageBitmap.hasAlpha(): Boolean = hasAlpha + +actual fun ImageBitmap.addLogo(): ImageBitmap = asAndroidBitmap().applyCanvas { + val radius = (width * 0.16f) / 2 + val paint = android.graphics.Paint() + paint.color = android.graphics.Color.WHITE + drawCircle(width / 2f, height / 2f, radius, paint) + val logo = androidAppContext.resources.getDrawable(R.drawable.icon_foreground_android_common, null).toBitmap() + val logoSize = (width * 0.24).toInt() + translate((width - logoSize) / 2f, (height - logoSize) / 2f) + drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null) +}.asImageBitmap() + +actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap = asAndroidBitmap().scale(width, height).asImageBitmap() + +actual fun isImage(uri: URI): Boolean = + MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true + +actual fun isAnimImage(uri: URI, drawable: Any?): Boolean { + val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable + val isAnimOldApi = Build.VERSION.SDK_INT < 28 && + (getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true) + return isAnimNewApi || isAnimOldApi +} + +actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap = + BitmapFactory.decodeStream(inputStream).asImageBitmap() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt new file mode 100644 index 0000000000..aca8efcb6f --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Log.android.kt @@ -0,0 +1,10 @@ +package chat.simplex.common.platform + +import android.util.Log + +actual object Log { + actual fun d(tag: String, text: String) = Log.d(tag, text).run{} + actual fun e(tag: String, text: String) = Log.e(tag, text).run{} + actual fun i(tag: String, text: String) = Log.i(tag, text).run{} + actual fun w(tag: String, text: String) = Log.w(tag, text).run{} +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt new file mode 100644 index 0000000000..41349654be --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -0,0 +1,25 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import com.google.accompanist.insets.navigationBarsWithImePadding + +actual fun Modifier.navigationBarsWithImePadding(): Modifier = navigationBarsWithImePadding() + +@Composable +actual fun ProvideWindowInsets( + consumeWindowInsets: Boolean, + windowInsetsAnimationsEnabled: Boolean, + content: @Composable () -> Unit +) { + com.google.accompanist.insets.ProvideWindowInsets(content = content) +} + +@Composable +actual fun Modifier.desktopOnExternalDrag( + enabled: Boolean, + onFiles: (List<String>) -> Unit, + onImage: (Painter) -> Unit, + onText: (String) -> Unit +): Modifier = this diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Notifications.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Notifications.android.kt new file mode 100644 index 0000000000..f0268219d2 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Notifications.android.kt @@ -0,0 +1,3 @@ +package chat.simplex.common.platform + +actual fun allowedToShowNotification(): Boolean = !isAppOnForeground diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt new file mode 100644 index 0000000000..10faa1a82b --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -0,0 +1,174 @@ +package chat.simplex.common.platform + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.text.InputType +import android.util.Log +import android.view.OnReceiveContentListener +import android.view.ViewGroup +import android.view.inputmethod.* +import android.widget.EditText +import android.widget.TextView +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.graphics.drawable.DrawableCompat +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.core.widget.doAfterTextChanged +import androidx.core.widget.doOnTextChanged +import chat.simplex.common.* +import chat.simplex.common.R +import chat.simplex.common.helpers.toURI +import chat.simplex.common.model.ChatModel +import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.helpers.SharedContent +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.delay +import java.lang.reflect.Field +import java.net.URI + +@Composable +actual fun PlatformTextField( + composeState: MutableState<ComposeState>, + sendMsgEnabled: Boolean, + textStyle: MutableState<TextStyle>, + showDeleteTextButton: MutableState<Boolean>, + userIsObserver: Boolean, + onMessageChange: (String) -> Unit, + onUpArrow: () -> Unit, + onDone: () -> Unit, +) { + val cs = composeState.value + val textColor = MaterialTheme.colors.onBackground + val tintColor = MaterialTheme.colors.secondaryVariant + val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) + val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } + val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } + val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() } + val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() } + var showKeyboard by remember { mutableStateOf(false) } + var freeFocus by remember { mutableStateOf(false) } + LaunchedEffect(cs.contextItem) { + if (cs.contextItem is ComposeContextItem.QuotedItem) { + delay(100) + showKeyboard = true + } else if (cs.contextItem is ComposeContextItem.EditingItem) { + // Keyboard will not show up if we try to show it too fast + delay(300) + showKeyboard = true + } + } + LaunchedEffect(sendMsgEnabled) { + if (!sendMsgEnabled) { + freeFocus = true + } + } + + AndroidView(modifier = Modifier, factory = { + val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { + override fun setOnReceiveContentListener( + mimeTypes: Array<out String>?, + listener: OnReceiveContentListener? + ) { + super.setOnReceiveContentListener(mimeTypes, listener) + } + + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { + val connection = super.onCreateInputConnection(editorInfo) + EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) + val onCommit = InputConnectionCompat.OnCommitContentListener { inputContentInfo, _, _ -> + try { + inputContentInfo.requestPermission() + } catch (e: Exception) { + return@OnCommitContentListener false + } + ChatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri.toURI())) + true + } + return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit) + } + } + editText.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + editText.maxLines = 16 + editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType + editText.setTextColor(textColor.toArgb()) + editText.textSize = textStyle.value.fontSize.value + val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!! + DrawableCompat.setTint(drawable, tintColor.toArgb()) + editText.background = drawable + editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) + editText.setText(cs.message) + if (Build.VERSION.SDK_INT >= 29) { + editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) } + } else { + try { + val f: Field = TextView::class.java.getDeclaredField("mCursorDrawableRes") + f.isAccessible = true + f.set(editText, R.drawable.edit_text_cursor) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + editText.doOnTextChanged { text, _, _, _ -> + if (!composeState.value.inProgress) { + onMessageChange(text.toString()) + } else if (text.toString() != composeState.value.message) { + editText.setText(composeState.value.message) + } + } + editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } + editText + }) { + it.setTextColor(textColor.toArgb()) + it.textSize = textStyle.value.fontSize.value + DrawableCompat.setTint(it.background, tintColor.toArgb()) + it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview + it.isFocusableInTouchMode = it.isFocusable + if (cs.message != it.text.toString()) { + it.setText(cs.message) + // Set cursor to the end of the text + it.setSelection(it.text.length) + } + if (showKeyboard) { + it.requestFocus() + val imm: InputMethodManager = androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) + showKeyboard = false + } + if (freeFocus) { + it.clearFocus() + hideKeyboard(it) + freeFocus = false + } + showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress + } + if (composeState.value.preview is ComposePreview.VoicePreview) { + ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) + } else if (userIsObserver) { + ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) + } +} + +@Composable +private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) { + Text( + generalGetString(textId), + Modifier.padding(padding), + color = MaterialTheme.colors.secondary, + style = textStyle.value.copy(fontStyle = FontStyle.Italic) + ) +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/RecAndPlay.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt similarity index 74% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/RecAndPlay.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 01387dd963..8df99d15f3 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/RecAndPlay.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -1,46 +1,33 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.platform -import android.app.Application import android.content.Context import android.media.* import android.media.AudioManager.AudioPlaybackCallback import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_DURATION_REACHED import android.media.MediaRecorder.MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED import android.os.Build -import android.util.Log import androidx.compose.runtime.* -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.ChatItem -import chat.simplex.app.views.helpers.AudioPlayer.duration +import chat.simplex.common.model.* +import chat.simplex.common.platform.AudioPlayer.duration +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.* import java.io.* -interface Recorder { - fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String - fun stop(): Int -} - -class RecorderNative(): Recorder { - companion object { - // Allows to stop the recorder from outside without having the recorder in a variable - var stopRecording: (() -> Unit)? = null - const val extension = "m4a" - } +actual class RecorderNative: RecorderInterface { private var recorder: MediaRecorder? = null private var progressJob: Job? = null private var filePath: String? = null private var recStartedAt: Long? = null private fun initRecorder() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - MediaRecorder(SimplexApp.context) + MediaRecorder(androidAppContext) } else { MediaRecorder() } override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String { - VideoPlayer.stopAll() + VideoPlayerHolder.stopAll() AudioPlayer.stop() val rec: MediaRecorder recorder = initRecorder().also { rec = it } @@ -51,8 +38,7 @@ class RecorderNative(): Recorder { rec.setAudioSamplingRate(16000) rec.setAudioEncodingBitRate(32000) rec.setMaxDuration(MAX_VOICE_MILLIS_FOR_SENDING) - val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE) - val fileToSave = File.createTempFile(generateNewFileName("voice", "${extension}_"), ".tmp", tmpDir) + val fileToSave = File.createTempFile(generateNewFileName("voice", "${RecorderInterface.extension}_"), ".tmp", tmpDir) fileToSave.deleteOnExit() val path = fileToSave.absolutePath filePath = path @@ -75,13 +61,13 @@ class RecorderNative(): Recorder { stop() } } - stopRecording = { stop() } + RecorderInterface.stopRecording = { stop() } return path } override fun stop(): Int { val path = filePath ?: return 0 - stopRecording = null + RecorderInterface.stopRecording = null runCatching { recorder?.stop() } @@ -110,7 +96,7 @@ class RecorderNative(): Recorder { private fun realDuration(path: String): Int? = duration(path) ?: progress() } -object AudioPlayer { +actual object AudioPlayer: AudioPlayerInterface { private val player = MediaPlayer().apply { setAudioAttributes( AudioAttributes.Builder() @@ -118,13 +104,13 @@ object AudioPlayer { .setUsage(AudioAttributes.USAGE_MEDIA) .build() ) - (SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager) + (androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager) .registerAudioPlaybackCallback(object: AudioPlaybackCallback() { override fun onPlaybackConfigChanged(configs: MutableList<AudioPlaybackConfiguration>?) { if (configs?.any { it.audioAttributes.usage == AudioAttributes.USAGE_VOICE_COMMUNICATION } == true) { // In a process of making a call - RecorderNative.stopRecording?.invoke() - stop() + RecorderInterface.stopRecording?.invoke() + AudioPlayer.stop() } super.onPlaybackConfigChanged(configs) } @@ -147,20 +133,25 @@ object AudioPlayer { } // Returns real duration of the track - private fun start(filePath: String, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { - if (!File(filePath).exists()) { - Log.e(TAG, "No such file: $filePath") + private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) + if (!File(absoluteFilePath).exists()) { + Log.e(TAG, "No such file: ${fileSource.filePath}") return null } - VideoPlayer.stopAll() - RecorderNative.stopRecording?.invoke() + VideoPlayerHolder.stopAll() + RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != filePath) { + if (current == null || current.first != fileSource.filePath) { stopListener() player.reset() runCatching { - player.setDataSource(filePath) + if (fileSource.cryptoArgs != null) { + player.setDataSource(CryptoMediaSource(readCryptoFile(absoluteFilePath, fileSource.cryptoArgs))) + } else { + player.setDataSource(absoluteFilePath) + } }.onFailure { Log.e(TAG, it.stackTraceToString()) AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) @@ -175,7 +166,7 @@ object AudioPlayer { } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = filePath to onProgressUpdate + currentlyPlaying.value = fileSource.filePath to onProgressUpdate progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { @@ -208,16 +199,16 @@ object AudioPlayer { return player.currentPosition } - fun stop() { + override fun stop() { if (currentlyPlaying.value == null) return player.stop() stopListener() } - fun stop(item: ChatItem) = stop(item.file?.fileName) + override fun stop(item: ChatItem) = stop(item.file?.fileName) // FileName or filePath are ok - fun stop(fileName: String?) { + override fun stop(fileName: String?) { if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) { stop() } @@ -241,8 +232,8 @@ object AudioPlayer { progressJob = null } - fun play( - filePath: String?, + override fun play( + fileSource: CryptoFile, audioPlaying: MutableState<Boolean>, progress: MutableState<Int>, duration: MutableState<Int>, @@ -251,7 +242,7 @@ object AudioPlayer { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(filePath ?: return, progress.value) { pro, state -> + val realDuration = start(fileSource, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -269,22 +260,22 @@ object AudioPlayer { realDuration?.let { duration.value = it } } - fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) { + override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) { pro.value = pause() audioPlaying.value = false } - fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { + override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { pro.value = ms - if (this.currentlyPlaying.value?.first == filePath) { + if (currentlyPlaying.value?.first == filePath) { player.seekTo(ms) } } - fun duration(filePath: String): Int? { + override fun duration(unencryptedFilePath: String): Int? { var res: Int? = null kotlin.runCatching { - helperPlayer.setDataSource(filePath) + helperPlayer.setDataSource(unencryptedFilePath) helperPlayer.prepare() helperPlayer.start() helperPlayer.stop() @@ -294,3 +285,23 @@ object AudioPlayer { return res } } + +actual typealias SoundPlayer = chat.simplex.common.helpers.SoundPlayer + +class CryptoMediaSource(val data: ByteArray) : MediaDataSource() { + override fun readAt(position: Long, buffer: ByteArray, offset: Int, size: Int): Int { + if (position >= data.size) return -1 + + val endPosition: Int = (position + size).toInt() + var sizeLeft: Int = size + if (endPosition > data.size) { + sizeLeft -= endPosition - data.size + } + + System.arraycopy(data, position.toInt(), buffer, offset, sizeLeft) + return sizeLeft + } + + override fun getSize(): Long = data.size.toLong() + override fun close() {} +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt new file mode 100644 index 0000000000..e15d1f9268 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -0,0 +1,53 @@ +package chat.simplex.common.platform + +import android.annotation.SuppressLint +import android.app.UiModeManager +import android.content.Context +import android.content.SharedPreferences +import android.content.res.Configuration +import android.text.BidiFormatter +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.AppPreferences +import com.russhwolf.settings.Settings +import com.russhwolf.settings.SharedPreferencesSettings +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.desc.desc + +@SuppressLint("DiscouragedApi") +@Composable +actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { + val context = LocalContext.current + val id = context.resources.getIdentifier(res, "font", context.packageName) + return Font(id, weight, style) +} + +actual fun StringResource.localized(): String = desc().toString(context = androidAppContext) + +actual fun isInNightMode() = + (androidAppContext.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES + +private val sharedPreferences: SharedPreferences by lazy { androidAppContext.getSharedPreferences(AppPreferences.SHARED_PREFS_ID, Context.MODE_PRIVATE) } +private val sharedPreferencesThemes: SharedPreferences by lazy { androidAppContext.getSharedPreferences(AppPreferences.SHARED_PREFS_THEMES_ID, Context.MODE_PRIVATE) } + +actual val settings: Settings by lazy { SharedPreferencesSettings(sharedPreferences) } +actual val settingsThemes: Settings by lazy { SharedPreferencesSettings(sharedPreferencesThemes) } + +actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.resources?.configuration?.orientation) { + Configuration.ORIENTATION_PORTRAIT -> WindowOrientation.PORTRAIT + Configuration.ORIENTATION_LANDSCAPE -> WindowOrientation.LANDSCAPE + else -> WindowOrientation.UNDEFINED +} + +@Composable +actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp + +actual fun desktopExpandWindowToWidth(width: Dp) {} + +actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt new file mode 100644 index 0000000000..a370bbf405 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Share.android.kt @@ -0,0 +1,113 @@ +package chat.simplex.common.platform + +import android.Manifest +import android.content.* +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.net.Uri +import android.provider.MediaStore +import android.webkit.MimeTypeMap +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.UriHandler +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import chat.simplex.common.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.* +import java.io.BufferedOutputStream +import java.io.File +import chat.simplex.res.MR +import java.io.ByteArrayOutputStream + +actual fun ClipboardManager.shareText(text: String) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + flags = FLAG_ACTIVITY_NEW_TASK + } + val shareIntent = Intent.createChooser(sendIntent, null) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + androidAppContext.startActivity(shareIntent) +} + +actual fun shareFile(text: String, fileSource: CryptoFile) { + val uri = if (fileSource.cryptoArgs != null) { + val tmpFile = File(tmpDir, fileSource.filePath) + tmpFile.deleteOnExit() + ChatModel.filesToDelete.add(tmpFile) + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, tmpFile.absolutePath) + FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(tmpFile.absolutePath)).toURI() + } else { + getAppFileUri(fileSource.filePath) + } + val ext = fileSource.filePath.substringAfterLast(".") + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext) ?: return + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + /*if (text.isNotEmpty()) { + putExtra(Intent.EXTRA_TEXT, text) + }*/ + putExtra(Intent.EXTRA_STREAM, uri.toUri()) + type = mimeType + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + val shareIntent = Intent.createChooser(sendIntent, null) + shareIntent.addFlags(FLAG_ACTIVITY_NEW_TASK) + androidAppContext.startActivity(shareIntent) +} + +actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { + val emailIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) + emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject) + emailIntent.putExtra(Intent.EXTRA_TEXT, body) + emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + try { + androidAppContext.startActivity(emailIntent) + } catch (e: ActivityNotFoundException) { + Log.e(TAG, "No activity was found for handling email intent") + } +} + +fun imageMimeType(fileName: String): String { + val lowercaseName = fileName.lowercase() + return when { + lowercaseName.endsWith(".png") -> "image/png" + lowercaseName.endsWith(".gif") -> "image/gif" + lowercaseName.endsWith(".webp") -> "image/webp" + lowercaseName.endsWith(".avif") -> "image/avif" + lowercaseName.endsWith(".svg") -> "image/svg+xml" + else -> "image/jpeg" + } +} + +/** Before calling, make sure the user allows to write to external storage [Manifest.permission.WRITE_EXTERNAL_STORAGE] */ +fun saveImage(ciFile: CIFile?) { + val filePath = getLoadedFilePath(ciFile) + val fileName = ciFile?.fileName + if (filePath != null && fileName != null) { + val values = ContentValues() + values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis()) + values.put(MediaStore.Images.Media.MIME_TYPE, imageMimeType(fileName)) + values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + values.put(MediaStore.MediaColumns.TITLE, fileName) + val uri = androidAppContext.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + uri?.let { + androidAppContext.contentResolver.openOutputStream(uri)?.let { stream -> + val outputStream = BufferedOutputStream(stream) + if (ciFile.fileSource?.cryptoArgs != null) { + createTmpFileAndDelete { tmpFile -> + decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath) + tmpFile.inputStream().use { it.copyTo(outputStream) } + } + outputStream.close() + } else { + File(filePath).inputStream().use { it.copyTo(outputStream) } + outputStream.close() + } + showToast(generalGetString(MR.strings.image_saved)) + } + } + } else { + showToast(generalGetString(MR.strings.file_not_found)) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt new file mode 100644 index 0000000000..c458561f96 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/UI.android.kt @@ -0,0 +1,71 @@ +package chat.simplex.common.platform + +import android.app.Activity +import android.content.Context +import android.content.pm.ActivityInfo +import android.graphics.Rect +import android.os.Build +import android.view.* +import android.view.inputmethod.InputMethodManager +import android.widget.Toast +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalView +import chat.simplex.common.views.helpers.KeyboardState +import androidx.compose.ui.platform.LocalContext as LocalContext1 + +actual fun showToast(text: String, timeout: Long) = Toast.makeText(androidAppContext, text, Toast.LENGTH_SHORT).show() + +@Composable +actual fun LockToCurrentOrientationUntilDispose() { + val context = LocalContext1.current + DisposableEffect(Unit) { + val activity = (context as Activity?) ?: return@DisposableEffect onDispose {} + val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager + val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation + activity.requestedOrientation = when (rotation) { + Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT + Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE + else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT + } + // Unlock orientation + onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } + } +} + +@Composable +actual fun LocalMultiplatformView(): Any? = LocalView.current + +@Composable +actual fun getKeyboardState(): State<KeyboardState> { + val keyboardState = remember { mutableStateOf(KeyboardState.Closed) } + val view = LocalView.current + DisposableEffect(view) { + val onGlobalListener = ViewTreeObserver.OnGlobalLayoutListener { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + val screenHeight = view.rootView.height + val keypadHeight = screenHeight - rect.bottom + keyboardState.value = if (keypadHeight > screenHeight * 0.15) { + KeyboardState.Opened + } else { + KeyboardState.Closed + } + } + view.viewTreeObserver.addOnGlobalLayoutListener(onGlobalListener) + + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(onGlobalListener) + } + } + + return keyboardState +} + +actual fun hideKeyboard(view: Any?) { + // LALAL + // LocalSoftwareKeyboardController.current?.hide() + if (view is View) { + (androidAppContext.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(view.windowToken, 0) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/VideoPlayer.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt similarity index 64% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/VideoPlayer.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt index fcaa6158df..9d5eadad72 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/VideoPlayer.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/VideoPlayer.android.kt @@ -1,82 +1,47 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.platform -import android.content.Context -import android.graphics.Bitmap +import android.media.MediaMetadataRetriever import android.media.session.PlaybackState import android.net.Uri -import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf -import chat.simplex.app.* -import chat.simplex.app.R +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import chat.simplex.common.helpers.toUri +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR import com.google.android.exoplayer2.* import com.google.android.exoplayer2.C.* import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.upstream.DefaultHttpDataSource -import chat.simplex.res.MR import kotlinx.coroutines.* import java.io.File +import java.net.URI -class VideoPlayer private constructor( - private val uri: Uri, - private val gallery: Boolean, - private val defaultPreview: Bitmap, +actual class VideoPlayer actual constructor( + override val uri: URI, + override val gallery: Boolean, + private val defaultPreview: ImageBitmap, defaultDuration: Long, - soundEnabled: Boolean, -) { - companion object { - private val players: MutableMap<Pair<Uri, Boolean>, VideoPlayer> = mutableMapOf() - private val previewsAndDurations: MutableMap<Uri, PreviewAndDuration> = mutableMapOf() - - fun getOrCreate( - uri: Uri, - gallery: Boolean, - defaultPreview: Bitmap, - defaultDuration: Long, - soundEnabled: Boolean, - ): VideoPlayer = - players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) } - - fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean = - player(fileName, gallery)?.enableSound(enable) == true - - private fun player(fileName: String?, gallery: Boolean): VideoPlayer? { - fileName ?: return null - return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery } - } - - fun release(uri: Uri, gallery: Boolean, remove: Boolean) = - player(uri.path, gallery)?.release(remove) - - fun stopAll() { - players.values.forEach { it.stop() } - } - - fun releaseAll() { - players.values.forEach { it.release(false) } - players.clear() - previewsAndDurations.clear() - } - } - - data class PreviewAndDuration(val preview: Bitmap?, val duration: Long?, val timestamp: Long) - + soundEnabled: Boolean +): VideoPlayerInterface { private val currentVolume: Float - val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled) - val brokenVideo: MutableState<Boolean> = mutableStateOf(false) - val videoPlaying: MutableState<Boolean> = mutableStateOf(false) - val progress: MutableState<Long> = mutableStateOf(0L) - val duration: MutableState<Long> = mutableStateOf(defaultDuration) - val preview: MutableState<Bitmap> = mutableStateOf(defaultPreview) + + override val soundEnabled: MutableState<Boolean> = mutableStateOf(soundEnabled) + override val brokenVideo: MutableState<Boolean> = mutableStateOf(false) + override val videoPlaying: MutableState<Boolean> = mutableStateOf(false) + override val progress: MutableState<Long> = mutableStateOf(0L) + override val duration: MutableState<Long> = mutableStateOf(defaultDuration) + override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview) init { setPreviewAndDuration() } - val player = ExoPlayer.Builder(SimplexApp.context, - DefaultRenderersFactory(SimplexApp.context)) + val player = ExoPlayer.Builder(androidAppContext, + DefaultRenderersFactory(androidAppContext)) /*.setLoadControl(DefaultLoadControl.Builder() .setPrioritizeTimeOverSizeThresholds(false) // Could probably save some megabytes in memory in case it will be needed .createDefaultLoadControl())*/ @@ -84,20 +49,20 @@ class VideoPlayer private constructor( .setSeekForwardIncrementMs(10_000) .build() .apply { - // Repeat the same track endlessly - repeatMode = Player.REPEAT_MODE_ONE - currentVolume = volume - if (!soundEnabled) { - volume = 0f + // Repeat the same track endlessly + repeatMode = Player.REPEAT_MODE_ONE + currentVolume = volume + if (!soundEnabled) { + volume = 0f + } + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(CONTENT_TYPE_MUSIC) + .setUsage(USAGE_MEDIA) + .build(), + true // disallow to play multiple instances simultaneously + ) } - setAudioAttributes( - AudioAttributes.Builder() - .setContentType(CONTENT_TYPE_MUSIC) - .setUsage(USAGE_MEDIA) - .build(), - true // disallow to play multiple instances simultaneously - ) - } private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null) private var progressJob: Job? = null @@ -115,14 +80,14 @@ class VideoPlayer private constructor( } if (soundEnabled.value) { - RecorderNative.stopRecording?.invoke() + RecorderInterface.stopRecording?.invoke() } AudioPlayer.stop() - stopAll() + VideoPlayerHolder.stopAll() if (listener.value == null) { runCatching { - val dataSourceFactory = DefaultDataSource.Factory(SimplexApp.context, DefaultHttpDataSource.Factory()) - val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri)) + val dataSourceFactory = DefaultDataSource.Factory(androidAppContext, DefaultHttpDataSource.Factory()) + val source = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(Uri.parse(uri.toString()))) player.setMediaSource(source, seek ?: 0L) }.onFailure { Log.e(TAG, it.stackTraceToString()) @@ -170,14 +135,14 @@ class VideoPlayer private constructor( override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) // Produce non-ideal transition from stopped to playing state while showing preview image in ChatView -// videoPlaying.value = isPlaying + // videoPlaying.value = isPlaying } }) return true } - fun stop() { + override fun stop() { player.stop() stopListener() } @@ -199,7 +164,7 @@ class VideoPlayer private constructor( progressJob = null } - fun play(resetOnEnd: Boolean) { + override fun play(resetOnEnd: Boolean) { if (progress.value == duration.value) { progress.value = 0 } @@ -218,24 +183,24 @@ class VideoPlayer private constructor( } } - fun enableSound(enable: Boolean): Boolean { + override fun enableSound(enable: Boolean): Boolean { if (soundEnabled.value == enable) return false soundEnabled.value = enable player.volume = if (enable) currentVolume else 0f return true } - fun release(remove: Boolean) { + override fun release(remove: Boolean) { player.release() if (remove) { - players.remove(uri to gallery) + VideoPlayerHolder.players.remove(uri to gallery) } } private fun setPreviewAndDuration() { // It freezes main thread, doing it in IO thread CoroutineScope(Dispatchers.IO).launch { - val previewAndDuration = previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) } + val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(uri) } withContext(Dispatchers.Main) { preview.value = previewAndDuration.preview ?: defaultPreview duration.value = (previewAndDuration.duration ?: 0) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt new file mode 100644 index 0000000000..0dcbb42322 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Theme.android.kt @@ -0,0 +1,6 @@ +package chat.simplex.common.ui.theme + +import androidx.compose.runtime.Composable + +@Composable +actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Type.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Type.android.kt new file mode 100644 index 0000000000..2e7404f241 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/ui/theme/Type.android.kt @@ -0,0 +1,15 @@ +package chat.simplex.common.ui.theme + +import androidx.compose.ui.text.font.* +import chat.simplex.res.MR + +actual val Inter: FontFamily = FontFamily( + Font(MR.fonts.Inter.regular.fontResourceId), + Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic), + Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold), + Font(MR.fonts.Inter.semibold.fontResourceId, FontWeight.SemiBold), + Font(MR.fonts.Inter.medium.fontResourceId, FontWeight.Medium), + Font(MR.fonts.Inter.light.fontResourceId, FontWeight.Light) +) + +actual val EmojiFont: FontFamily = FontFamily.Default diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallView.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt similarity index 90% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallView.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index f937342f55..260182b5a4 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallView.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.call +package chat.simplex.common.views.call import android.Manifest import android.annotation.SuppressLint @@ -9,11 +9,12 @@ import android.media.* import android.os.Build import android.os.PowerManager import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK -import android.util.Log import android.view.ViewGroup import android.webkit.* -import androidx.activity.compose.BackHandler +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -26,22 +27,20 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewClientCompat -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ProfileImage -import chat.simplex.app.views.helpers.withApi -import chat.simplex.app.views.usersettings.NotificationsMode -import com.google.accompanist.permissions.rememberMultiplePermissionsState +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.Contact +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import com.google.accompanist.permissions.rememberMultiplePermissionsState import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* import kotlinx.serialization.decodeFromString @@ -49,20 +48,21 @@ import kotlinx.serialization.encodeToString @SuppressLint("SourceLockedOrientationActivity") @Composable -fun ActiveCallView(chatModel: ChatModel) { +actual fun ActiveCallView() { + val chatModel = ChatModel BackHandler(onBack = { val call = chatModel.activeCall.value if (call != null) withApi { chatModel.callManager.endCall(call) } }) val audioViaBluetooth = rememberSaveable { mutableStateOf(false) } - val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE.name } + val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE } LaunchedEffect(Unit) { // Start service when call happening since it's not already started. // It's needed to prevent Android from shutting down a microphone after a minute or so when screen is off - if (!ntfModeService) SimplexService.start() + if (!ntfModeService) platform.androidServiceStart() } DisposableEffect(Unit) { - val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager var btDeviceCount = 0 val audioCallback = object: AudioDeviceCallback() { override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) { @@ -89,16 +89,16 @@ fun ActiveCallView(chatModel: ChatModel) { } } am.registerAudioDeviceCallback(audioCallback, null) - val pm = (SimplexApp.context.getSystemService(Context.POWER_SERVICE) as PowerManager) + val pm = (androidAppContext.getSystemService(Context.POWER_SERVICE) as PowerManager) val proximityLock = if (pm.isWakeLockLevelSupported(PROXIMITY_SCREEN_OFF_WAKE_LOCK)) { - pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, SimplexApp.context.packageName + ":proximityLock") + pm.newWakeLock(PROXIMITY_SCREEN_OFF_WAKE_LOCK, androidAppContext.packageName + ":proximityLock") } else { null } proximityLock?.acquire() onDispose { // Stop it when call ended - if (!ntfModeService) SimplexService.safeStopService(SimplexApp.context) + if (!ntfModeService) platform.androidServiceSafeStop() dropAudioManagerOverrides() am.unregisterAudioDeviceCallback(audioCallback) proximityLock?.release() @@ -217,7 +217,7 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot } private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boolean>) { - val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager Log.d(TAG, "setCallSound: set audio mode, speaker enabled: $speaker") am.mode = AudioManager.MODE_IN_COMMUNICATION if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -243,7 +243,7 @@ private fun setCallSound(speaker: Boolean, audioViaBluetooth: MutableState<Boole } private fun dropAudioManagerOverrides() { - val am = SimplexApp.context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + val am = androidAppContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager am.mode = AudioManager.MODE_NORMAL // Clear selected communication device to default value after we changed it in call if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -268,7 +268,9 @@ private fun ActiveCallOverlayLayout( when (call.peerMedia ?: call.localMedia) { CallMediaType.Video -> { CallInfoView(call, alignment = Alignment.Start) - Spacer(Modifier.fillMaxHeight().weight(1f)) + Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { + DisabledBackgroundCallsButton() + } Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { ToggleAudioButton(call, toggleAudio) Spacer(Modifier.size(40.dp)) @@ -294,7 +296,9 @@ private fun ActiveCallOverlayLayout( ProfileImage(size = 192.dp, image = call.contact.profile.image) CallInfoView(call, alignment = Alignment.CenterHorizontally) } - Spacer(Modifier.fillMaxHeight().weight(1f)) + Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) { + DisabledBackgroundCallsButton() + } Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) { Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { IconButton(onClick = dismiss) { @@ -353,12 +357,37 @@ fun CallInfoView(call: Call, alignment: Alignment.Horizontal) { InfoText(call.callState.text) val connInfo = call.connectionInfo -// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})" + // val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})" val connInfoText = if (connInfo == null) "" else " (${connInfo.text})" InfoText(call.encryptionStatus + connInfoText) } } +@Composable +private fun DisabledBackgroundCallsButton() { + var show by remember { mutableStateOf(!platform.androidIsBackgroundCallAllowed()) } + if (show) { + Row( + Modifier + .padding(bottom = 24.dp) + .clickable { + withBGApi { + show = !platform.androidAskToAllowBackgroundCalls() + } + } + .background(WarningOrange.copy(0.3f), RoundedCornerShape(50)) + .padding(start = 14.dp, top = 4.dp, end = 8.dp, bottom = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(stringResource(MR.strings.system_restricted_background_in_call_title), color = WarningOrange) + Spacer(Modifier.width(8.dp)) + IconButton(onClick = { show = false }, Modifier.size(24.dp)) { + Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) + } + } + } +} + //@Composable //fun CallViewDebug(close: () -> Unit) { // val callCommand = remember { mutableStateOf<WCallCommand?>(null)} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeView.android.kt new file mode 100644 index 0000000000..719dfeea55 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ComposeView.android.kt @@ -0,0 +1,81 @@ +package chat.simplex.common.views.chat + +import android.Manifest +import android.content.ActivityNotFoundException +import android.content.pm.PackageManager +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.ImageBitmap +import androidx.core.content.ContextCompat +import chat.simplex.common.helpers.toURI +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import java.net.URI + +@Composable +actual fun AttachmentSelection( + composeState: MutableState<ComposeState>, + attachmentOption: MutableState<AttachmentOption?>, + processPickedFile: (URI?, String?) -> Unit, + processPickedMedia: (List<URI>, String?) -> Unit +) { + val cameraLauncher = rememberCameraLauncher { uri: Uri? -> + if (uri != null) { + val bitmap: ImageBitmap? = getBitmapFromUri(uri.toURI()) + if (bitmap != null) { + val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) + composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri.toURI())))) + } + } + } + val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean -> + if (isGranted) { + cameraLauncher.launchWithFallback() + } else { + showToast(generalGetString(MR.strings.toast_permission_denied)) + } + } + val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it.map { it.toURI() }, null) } + val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it.map { it.toURI() }, null) } + val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it.map { it.toURI() }, null) } + val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it.map { it.toURI() }, null) } + val filesLauncher = rememberGetContentLauncher { processPickedFile(it?.toURI(), null) } + LaunchedEffect(attachmentOption.value) { + when (attachmentOption.value) { + AttachmentOption.CameraPhoto -> { + when (PackageManager.PERMISSION_GRANTED) { + ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) -> { + cameraLauncher.launchWithFallback() + } + else -> { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } + } + attachmentOption.value = null + } + AttachmentOption.GalleryImage -> { + try { + galleryImageLauncher.launch(0) + } catch (e: ActivityNotFoundException) { + galleryImageLauncherFallback.launch("image/*") + } + attachmentOption.value = null + } + AttachmentOption.GalleryVideo -> { + try { + galleryVideoLauncher.launch(0) + } catch (e: ActivityNotFoundException) { + galleryVideoLauncherFallback.launch("video/*") + } + attachmentOption.value = null + } + AttachmentOption.File -> { + filesLauncher.launch("*/*") + attachmentOption.value = null + } + else -> {} + } + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.android.kt new file mode 100644 index 0000000000..79361dc073 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.android.kt @@ -0,0 +1,16 @@ +package chat.simplex.common.views.chat + +import android.Manifest +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import chat.simplex.common.views.chat.ScanCodeLayout +import com.google.accompanist.permissions.rememberPermissionState + +@Composable +actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) + LaunchedEffect(Unit) { + cameraPermissionState.launchPermissionRequest() + } + ScanCodeLayout(verifyCode, close) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/SendMsgView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/SendMsgView.android.kt new file mode 100644 index 0000000000..8fb2263c7b --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/SendMsgView.android.kt @@ -0,0 +1,17 @@ +package chat.simplex.common.views.chat + +import android.Manifest +import androidx.compose.runtime.Composable +import com.google.accompanist.permissions.rememberMultiplePermissionsState + +@Composable +actual fun allowedToRecordVoiceByPlatform(): Boolean { + val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO)) + return permissionsState.allPermissionsGranted +} + +@Composable +actual fun VoiceButtonWithoutPermissionByPlatform() { + val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO)) + VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt new file mode 100644 index 0000000000..28c00ec018 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -0,0 +1,53 @@ +package chat.simplex.common.views.chat.item + +import android.os.Build.VERSION.SDK_INT +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalContext +import chat.simplex.common.helpers.toUri +import chat.simplex.common.model.CIFile +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.ModalManager +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import java.net.URI + +@Composable +actual fun SimpleAndAnimatedImageView( + data: ByteArray, + imageBitmap: ImageBitmap, + file: CIFile?, + imageProvider: () -> ImageGalleryProvider, + ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit +) { + val context = LocalContext.current + val imagePainter = rememberAsyncImagePainter( + ImageRequest.Builder(context).data(data = data).size(coil.size.Size.ORIGINAL).build(), + placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil + imageLoader = imageLoader + ) + val view = LocalMultiplatformView() + ImageView(imagePainter) { + hideKeyboard(view) + if (getLoadedFilePath(file) != null) { + ModalManager.fullscreen.showCustomModal(animated = false) { close -> + ImageFullScreenView(imageProvider, close) + } + } + } +} + +private val imageLoader = ImageLoader.Builder(androidAppContext) + .components { + if (SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt new file mode 100644 index 0000000000..f2f3e27766 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.android.kt @@ -0,0 +1,46 @@ +package chat.simplex.common.views.chat.item + +import android.graphics.Rect +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import chat.simplex.common.platform.VideoPlayer +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH +import com.google.android.exoplayer2.ui.StyledPlayerView + +@Composable +actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) { + AndroidView( + factory = { ctx -> + StyledPlayerView(ctx).apply { + useController = false + resizeMode = RESIZE_MODE_FIXED_WIDTH + this.player = player.player + } + }, + Modifier + .width(width) + .combinedClickable( + onLongClick = onLongClick, + onClick = { if (player.player.playWhenReady) stop() else onClick() } + ) + ) +} + +@Composable +actual fun LocalWindowWidth(): Dp { + val view = LocalView.current + val density = LocalDensity.current.density + return remember { + val rect = Rect() + view.getWindowVisibleDisplayFrame(rect) + (rect.width() / density).dp + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt new file mode 100644 index 0000000000..8bb70c4a09 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.android.kt @@ -0,0 +1,43 @@ +package chat.simplex.common.views.chat.item + +import android.Manifest +import android.os.Build +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.MsgContent +import chat.simplex.common.platform.FileChooserLauncher +import chat.simplex.common.platform.saveImage +import chat.simplex.common.views.helpers.SharedContent +import chat.simplex.common.views.helpers.withApi +import chat.simplex.res.MR +import com.google.accompanist.permissions.rememberPermissionState +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun ReactionIcon(text: String, fontSize: TextUnit) { + Text(text, fontSize = fontSize) +} + +@Composable +actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) { + val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) + ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { + when (cItem.content.msgContent) { + is MsgContent.MCImage -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) { + saveImage(cItem.file) + } else { + writePermissionState.launchPermissionRequest() + } + } + is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } + else -> {} + } + showMenu.value = false + }) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.android.kt new file mode 100644 index 0000000000..f0e8c12952 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.android.kt @@ -0,0 +1,10 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable + +@Composable +actual fun EmojiText(text: String) { + val s = text.trim() + Text(s, style = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt new file mode 100644 index 0000000000..d4efdc3e59 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.android.kt @@ -0,0 +1,75 @@ +package chat.simplex.common.views.chat.item + +import android.os.Build +import android.view.View +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.isVisible +import chat.simplex.common.helpers.toUri +import chat.simplex.common.platform.VideoPlayer +import chat.simplex.res.MR +import coil.ImageLoader +import coil.compose.rememberAsyncImagePainter +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder +import coil.request.ImageRequest +import coil.size.Size +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout +import com.google.android.exoplayer2.ui.StyledPlayerView +import dev.icerock.moko.resources.compose.stringResource +import java.net.URI + +@Composable +actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) { + // I'm making a new instance of imageLoader here because if I use one instance in multiple places + // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want + val imageLoader = ImageLoader.Builder(LocalContext.current) + .components { + if (Build.VERSION.SDK_INT >= 28) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } + .build() + Image( + rememberAsyncImagePainter( + ImageRequest.Builder(LocalContext.current).data(data = data).size(Size.ORIGINAL).build(), + placeholder = BitmapPainter(imageBitmap), // show original image while it's still loading by coil + imageLoader = imageLoader + ), + contentDescription = stringResource(MR.strings.image_descr), + contentScale = ContentScale.Fit, + modifier = modifier, + ) +} + +@Composable +actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) { + AndroidView( + factory = { ctx -> + StyledPlayerView(ctx).apply { + resizeMode = if (ctx.resources.configuration.screenWidthDp > ctx.resources.configuration.screenHeightDp) { + AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT + } else { + AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH + } + setShowPreviousButton(false) + setShowNextButton(false) + setShowSubtitleButton(false) + setShowVrButton(false) + controllerAutoShow = false + findViewById<View>(com.google.android.exoplayer2.R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb()) + findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false + this.player = player.player + } + }, + modifier + ) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt new file mode 100644 index 0000000000..df2499926f --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.android.kt @@ -0,0 +1,106 @@ +package chat.simplex.common.views.database + +import SectionItemView +import SectionTextFooter +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.SimplexGreen +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun SavePassphraseSetting( + useKeychain: Boolean, + initialRandomDBPassphrase: Boolean, + storedKey: Boolean, + progressIndicator: Boolean, + minHeight: Dp, + onCheckedChange: (Boolean) -> Unit, +) { + SectionItemView(minHeight = minHeight) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled), + stringResource(MR.strings.save_passphrase_in_keychain), + tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + stringResource(MR.strings.save_passphrase_in_keychain), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = useKeychain, + onCheckedChange = onCheckedChange, + enabled = !initialRandomDBPassphrase && !progressIndicator + ) + } + } +} + +@Composable +actual fun DatabaseEncryptionFooter( + useKeychain: MutableState<Boolean>, + chatDbEncrypted: Boolean?, + storedKey: MutableState<Boolean>, + initialRandomDBPassphrase: MutableState<Boolean>, +) { + if (chatDbEncrypted == false) { + SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted)) + } else if (useKeychain.value) { + if (storedKey.value) { + SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely)) + if (initialRandomDBPassphrase.value) { + SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase)) + } else { + SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) + } + } else { + SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs)) + } + } else { + SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) + SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) + } +} + +actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.encrypt_database_question), + text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(), + confirmText = generalGetString(MR.strings.encrypt_database), + onConfirm = onConfirm, + destructive = true, + ) +} + +actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.change_database_passphrase_question), + text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(), + confirmText = generalGetString(MR.strings.update_database), + onConfirm = onConfirm, + destructive = false, + ) +} + +actual fun removePassphraseAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.remove_passphrase_from_keychain), + text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(), + confirmText = generalGetString(MR.strings.remove_passphrase), + onConfirm = onConfirm, + destructive = true, + ) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.android.kt new file mode 100644 index 0000000000..3dace5f9bc --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.android.kt @@ -0,0 +1,30 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import chat.simplex.common.views.newchat.ActionButton +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun ChooseAttachmentButtons(attachmentOption: MutableState<AttachmentOption?>, hide: () -> Unit) { + ActionButton(Modifier.fillMaxWidth(0.25f), null, stringResource(MR.strings.use_camera_button), icon = painterResource(MR.images.ic_camera_enhance)) { + attachmentOption.value = AttachmentOption.CameraPhoto + hide() + } + ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(MR.strings.gallery_image_button), icon = painterResource(MR.images.ic_add_photo)) { + attachmentOption.value = AttachmentOption.GalleryImage + hide() + } + ActionButton(Modifier.fillMaxWidth(0.50f), null, stringResource(MR.strings.gallery_video_button), icon = painterResource(MR.images.ic_smart_display)) { + attachmentOption.value = AttachmentOption.GalleryVideo + hide() + } + ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(MR.strings.choose_file), icon = painterResource(MR.images.ic_note_add)) { + attachmentOption.value = AttachmentOption.File + hide() + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.android.kt new file mode 100644 index 0000000000..0b99e07582 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.android.kt @@ -0,0 +1,16 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.runtime.Composable +import androidx.compose.ui.window.Dialog + +@Composable +actual fun DefaultDialog( + onDismissRequest: () -> Unit, + content: @Composable () -> Unit +) { + Dialog( + onDismissRequest = onDismissRequest + ) { + content() + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/DefaultDropDownMenu.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/DefaultDropDownMenu.android.kt new file mode 100644 index 0000000000..38dd78dd99 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/DefaultDropDownMenu.android.kt @@ -0,0 +1,48 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties + +actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this + +actual interface DefaultExposedDropdownMenuBoxScope { + @Composable + actual fun DefaultExposedDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier, + content: @Composable ColumnScope.() -> Unit + ) { + DropdownMenu(expanded, onDismissRequest, modifier, content = content) + } + + @Composable + fun DropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + offset: DpOffset = DpOffset(0.dp, 0.dp), + properties: PopupProperties = PopupProperties(focusable = true), + content: @Composable ColumnScope.() -> Unit + ) { + androidx.compose.material.DropdownMenu(expanded, onDismissRequest, modifier, offset, properties, content) + } +} + +@Composable +actual fun DefaultExposedDropdownMenuBox( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier, + content: @Composable DefaultExposedDropdownMenuBoxScope.() -> Unit +) { + val scope = remember { object : DefaultExposedDropdownMenuBoxScope {} } + androidx.compose.material.ExposedDropdownMenuBox(expanded, onExpandedChange, modifier, content = { + scope.content() + }) +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt similarity index 69% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt index 2516bd7f82..98d1f8fb19 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/GetImageView.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/GetImageView.android.kt @@ -1,8 +1,7 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import android.Manifest import android.app.Activity -import android.app.Application import android.content.* import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.content.pm.PackageManager @@ -10,8 +9,6 @@ import android.graphics.* import android.net.Uri import android.provider.MediaStore import android.util.Base64 -import android.util.Log -import android.widget.Toast import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContract @@ -23,103 +20,35 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.platform.LocalContext import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.content.FileProvider -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.json -import chat.simplex.app.views.chat.PickFromGallery -import chat.simplex.app.views.newchat.ActionButton +import chat.simplex.common.helpers.APPLICATION_ID +import chat.simplex.common.helpers.toURI +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.json +import chat.simplex.common.platform.* +import chat.simplex.common.views.newchat.ActionButton import chat.simplex.res.MR import kotlinx.serialization.builtins.* -import java.io.ByteArrayOutputStream import java.io.File -import kotlin.math.min -import kotlin.math.sqrt - -// Inspired by https://github.com/MakeItEasyDev/Jetpack-Compose-Capture-Image-Or-Choose-from-Gallery -fun cropToSquare(image: Bitmap): Bitmap { - var xOffset = 0 - var yOffset = 0 - val side = min(image.height, image.width) - if (image.height < image.width) { - xOffset = (image.width - side) / 2 - } else { - yOffset = (image.height - side) / 2 - } - return Bitmap.createBitmap(image, xOffset, yOffset, side, side) -} - -fun resizeImageToStrSize(image: Bitmap, maxDataSize: Long): String { - var img = image - var str = compressImageStr(img) - while (str.length > maxDataSize) { - val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble()) - val clippedRatio = min(ratio, 2.0) - val width = (img.width.toDouble() / clippedRatio).toInt() - val height = img.height * width / img.width - img = Bitmap.createScaledBitmap(img, width, height, true) - str = compressImageStr(img) - } - return str -} - -private fun compressImageStr(bitmap: Bitmap): String { - val usePng = bitmap.hasAlpha() - val ext = if (usePng) "png" else "jpg" - return "data:image/$ext;base64," + Base64.encodeToString(compressImageData(bitmap, usePng).toByteArray(), Base64.NO_WRAP) -} - -fun resizeImageToDataSize(image: Bitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream { - var img = image - var stream = compressImageData(img, usePng) - while (stream.size() > maxDataSize) { - val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble()) - val clippedRatio = min(ratio, 2.0) - val width = (img.width.toDouble() / clippedRatio).toInt() - val height = img.height * width / img.width - img = Bitmap.createScaledBitmap(img, width, height, true) - stream = compressImageData(img, usePng) - } - return stream -} - -private fun compressImageData(bitmap: Bitmap, usePng: Boolean): ByteArrayOutputStream { - val stream = ByteArrayOutputStream() - bitmap.compress(if (!usePng) Bitmap.CompressFormat.JPEG else Bitmap.CompressFormat.PNG, 85, stream) - return stream -} +import java.net.URI val errorBitmapBytes = Base64.decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==", Base64.NO_WRAP) val errorBitmap: Bitmap = BitmapFactory.decodeByteArray(errorBitmapBytes, 0, errorBitmapBytes.size) -fun base64ToBitmap(base64ImageString: String): Bitmap { - val imageString = base64ImageString - .removePrefix("data:image/png;base64,") - .removePrefix("data:image/jpg;base64,") - try { - val imageBytes = Base64.decode(imageString, Base64.NO_WRAP) - return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) - } catch (e: Exception) { - Log.e(TAG, "base64ToBitmap error: $e") - return errorBitmap - } -} - class CustomTakePicturePreview(var uri: Uri?, var tmpFile: File?): ActivityResultContract<Void?, Uri?>() { @CallSuper override fun createIntent(context: Context, input: Void?): Intent { - val tmpDir = SimplexApp.context.getDir("temp", Application.MODE_PRIVATE) tmpFile = File.createTempFile("image", ".bmp", tmpDir) // Since the class should return Uri, the file should be deleted somewhere else. And in order to be sure, delegate this to system tmpFile?.deleteOnExit() ChatModel.filesToDelete.add(tmpFile!!) - uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile!!) + uri = FileProvider.getUriForFile(context, "$APPLICATION_ID.provider", tmpFile!!) return Intent(MediaStore.ACTION_IMAGE_CAPTURE) .putExtra(MediaStore.EXTRA_OUTPUT, uri) } @@ -201,7 +130,7 @@ fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() { try { // Try to open any camera just to capture an image, will not be returned like with previous intent - SimplexApp.context.startActivity(Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).also { it.addFlags(FLAG_ACTIVITY_NEW_TASK) }) + androidAppContext.startActivity(Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA).also { it.addFlags(FLAG_ACTIVITY_NEW_TASK) }) } catch (e: ActivityNotFoundException) { // No camera apps available at all Log.e(TAG, "Camera launcher2: " + e.stackTraceToString()) @@ -210,14 +139,15 @@ fun ManagedActivityResultLauncher<Void?, Uri?>.launchWithFallback() { } @Composable -fun GetImageBottomSheet( - imageBitmap: MutableState<Uri?>, - onImageChange: (Bitmap) -> Unit, +actual fun GetImageBottomSheet( + imageBitmap: MutableState<URI?>, + onImageChange: (ImageBitmap) -> Unit, hideBottomSheet: () -> Unit ) { val context = LocalContext.current val processPickedImage = { uri: Uri? -> if (uri != null) { + val uri = uri.toURI() val bitmap = getBitmapFromUri(uri) if (bitmap != null) { imageBitmap.value = uri @@ -233,7 +163,7 @@ fun GetImageBottomSheet( cameraLauncher.launchWithFallback() hideBottomSheet() } else { - Toast.makeText(context, generalGetString(MR.strings.toast_permission_denied), Toast.LENGTH_SHORT).show() + showToast(generalGetString(MR.strings.toast_permission_denied)) } } @@ -273,3 +203,65 @@ fun GetImageBottomSheet( } } } + +class PickFromGallery: ActivityResultContract<Int, Uri?>() { + override fun createIntent(context: Context, input: Int) = + Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { + type = "image/*" + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data +} + +class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() { + override fun createIntent(context: Context, input: Int) = + Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + type = "image/*" + } + + override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> = + if (intent?.data != null) + listOf(intent.data!!) + else if (intent?.clipData != null) + with(intent.clipData!!) { + val uris = ArrayList<Uri>() + for (i in 0 until kotlin.math.min(itemCount, 10)) { + val uri = getItemAt(i).uri + if (uri != null) uris.add(uri) + } + if (itemCount > 10) { + AlertManager.shared.showAlertMsg(MR.strings.images_limit_title, MR.strings.images_limit_desc) + } + uris + } + else + emptyList() +} + + +class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() { + override fun createIntent(context: Context, input: Int) = + Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI).apply { + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + type = "video/*" + } + + override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> = + if (intent?.data != null) + listOf(intent.data!!) + else if (intent?.clipData != null) + with(intent.clipData!!) { + val uris = ArrayList<Uri>() + for (i in 0 until kotlin.math.min(itemCount, 10)) { + val uri = getItemAt(i).uri + if (uri != null) uris.add(uri) + } + if (itemCount > 10) { + AlertManager.shared.showAlertMsg(MR.strings.videos_limit_title, MR.strings.videos_limit_desc) + } + uris + } + else + emptyList() +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt new file mode 100644 index 0000000000..b238bdf7ca --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.android.kt @@ -0,0 +1,81 @@ +package chat.simplex.common.views.helpers + +import android.os.Build.VERSION.SDK_INT +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.* +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity +import chat.simplex.common.platform.mainActivity +import chat.simplex.common.views.usersettings.LAMode + +actual fun authenticate( + promptTitle: String, + promptSubtitle: String, + selfDestruct: Boolean, + usingLAMode: LAMode, + completed: (LAResult) -> Unit +) { + val activity = mainActivity.get() ?: return completed(LAResult.Error("")) + when (usingLAMode) { + LAMode.SYSTEM -> when { + SDK_INT in 28..29 -> + // KeyguardManager.isDeviceSecure()? https://developer.android.com/training/sign-in/biometric-auth#declare-supported-authentication-types + authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_WEAK or DEVICE_CREDENTIAL) + SDK_INT > 29 -> + authenticateWithBiometricManager(promptTitle, promptSubtitle, activity, completed, BIOMETRIC_STRONG or DEVICE_CREDENTIAL) + else -> completed(LAResult.Unavailable()) + } + LAMode.PASSCODE -> { + authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed) + } + } +} + +private fun authenticateWithBiometricManager( + promptTitle: String, + promptSubtitle: String, + activity: FragmentActivity, + completed: (LAResult) -> Unit, + authenticators: Int +) { + val biometricManager = BiometricManager.from(activity) + when (biometricManager.canAuthenticate(authenticators)) { + BiometricManager.BIOMETRIC_SUCCESS -> { + val executor = ContextCompat.getMainExecutor(activity) + val biometricPrompt = BiometricPrompt( + activity, + executor, + object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError( + errorCode: Int, + errString: CharSequence + ) { + super.onAuthenticationError(errorCode, errString) + completed(LAResult.Error(errString)) + } + + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult + ) { + super.onAuthenticationSucceeded(result) + completed(LAResult.Success) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + completed(LAResult.Failed()) + } + } + ) + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle(promptTitle) + .setSubtitle(promptSubtitle) + .setAllowedAuthenticators(authenticators) + .setConfirmationRequired(false) + .build() + biometricPrompt.authenticate(promptInfo) + } + else -> completed(LAResult.Unavailable()) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt new file mode 100644 index 0000000000..127f13cd5c --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -0,0 +1,327 @@ +package chat.simplex.common.views.helpers + +import android.content.res.Resources +import android.graphics.* +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.media.MediaMetadataRetriever +import android.os.* +import android.provider.OpenableColumns +import android.text.Spanned +import android.text.SpannedString +import android.text.style.* +import android.util.Base64 +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.* +import androidx.core.content.FileProvider +import androidx.core.text.HtmlCompat +import chat.simplex.common.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import java.io.* +import java.net.URI + +fun Spanned.toHtmlWithoutParagraphs(): String { + return HtmlCompat.toHtml(this, HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE) + .substringAfter("<p dir=\"ltr\">").substringBeforeLast("</p>") +} + +fun Resources.getText(id: StringResource, vararg args: Any): CharSequence { + val escapedArgs = args.map { + if (it is Spanned) it.toHtmlWithoutParagraphs() else it + }.toTypedArray() + val resource = SpannedString(getText(id)) + val htmlResource = resource.toHtmlWithoutParagraphs() + val formattedHtml = String.format(htmlResource, *escapedArgs) + return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) +} + +actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { + return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) +} + +private fun spannableStringToAnnotatedString( + text: CharSequence, + density: Density, +): AnnotatedString { + return if (text is Spanned) { + with(density) { + buildAnnotatedString { + append((text.toString())) + text.getSpans(0, text.length, Any::class.java).forEach { + val start = text.getSpanStart(it) + val end = text.getSpanEnd(it) + when (it) { + is StyleSpan -> when (it.style) { + Typeface.NORMAL -> addStyle( + SpanStyle( + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + ), + start, + end + ) + Typeface.BOLD -> addStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Normal + ), + start, + end + ) + Typeface.ITALIC -> addStyle( + SpanStyle( + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Italic + ), + start, + end + ) + Typeface.BOLD_ITALIC -> addStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + fontStyle = FontStyle.Italic + ), + start, + end + ) + } + is TypefaceSpan -> addStyle( + SpanStyle( + fontFamily = when (it.family) { + FontFamily.SansSerif.name -> FontFamily.SansSerif + FontFamily.Serif.name -> FontFamily.Serif + FontFamily.Monospace.name -> FontFamily.Monospace + FontFamily.Cursive.name -> FontFamily.Cursive + else -> FontFamily.Default + } + ), + start, + end + ) + is AbsoluteSizeSpan -> addStyle( + SpanStyle(fontSize = if (it.dip) it.size.dp.toSp() else it.size.toSp()), + start, + end + ) + is RelativeSizeSpan -> addStyle( + SpanStyle(fontSize = it.sizeChange.em), + start, + end + ) + is StrikethroughSpan -> addStyle( + SpanStyle(textDecoration = TextDecoration.LineThrough), + start, + end + ) + is UnderlineSpan -> addStyle( + SpanStyle(textDecoration = TextDecoration.Underline), + start, + end + ) + is SuperscriptSpan -> addStyle( + SpanStyle(baselineShift = BaselineShift.Superscript), + start, + end + ) + is SubscriptSpan -> addStyle( + SpanStyle(baselineShift = BaselineShift.Subscript), + start, + end + ) + is ForegroundColorSpan -> addStyle( + SpanStyle(color = Color(it.foregroundColor)), + start, + end + ) + else -> addStyle(SpanStyle(color = Color.White), start, end) + } + } + } + } + } else { + AnnotatedString(text.toString()) + } +} + +actual fun getAppFileUri(fileName: String): URI = + FileProvider.getUriForFile(androidAppContext, "$APPLICATION_ID.provider", File(getAppFilePath(fileName))).toURI() + +// https://developer.android.com/training/data-storage/shared/documents-files#bitmap +actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? { + val filePath = getLoadedFilePath(file) + return if (filePath != null && file != null) { + try { + val data = if (file.fileSource?.cryptoArgs != null) { + readCryptoFile(getAppFilePath(file.fileSource.filePath), file.fileSource.cryptoArgs) + } else { + File(getAppFilePath(file.fileName)).readBytes() + } + decodeSampledBitmapFromByteArray(data, 1000, 1000).asImageBitmap() to data + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + null + } + } else { + null + } +} + +// https://developer.android.com/topic/performance/graphics/load-bitmap#load-bitmap +private fun decodeSampledBitmapFromByteArray(data: ByteArray, reqWidth: Int, reqHeight: Int): Bitmap { + // First decode with inJustDecodeBounds=true to check dimensions + return BitmapFactory.Options().run { + inJustDecodeBounds = true + BitmapFactory.decodeByteArray(data, 0, data.size) + // Calculate inSampleSize + inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) + // Decode bitmap with inSampleSize set + inJustDecodeBounds = false + + BitmapFactory.decodeByteArray(data, 0, data.size) + } +} + +private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { + // Raw height and width of image + val (height: Int, width: Int) = options.run { outHeight to outWidth } + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + val halfHeight: Int = height / 2 + val halfWidth: Int = width / 2 + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize +} + +actual fun getFileName(uri: URI): String? { + return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + cursor.getString(nameIndex) + } +} + +actual fun getAppFilePath(uri: URI): String? { + return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + getAppFilePath(cursor.getString(nameIndex)) + } +} + +actual fun getFileSize(uri: URI): Long? { + return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + cursor.getLong(sizeIndex) + } +} + +actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? { + return if (Build.VERSION.SDK_INT >= 28) { + val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) + try { + ImageDecoder.decodeBitmap(source) + } catch (e: android.graphics.ImageDecoder.DecodeException) { + Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}") + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.image_decoding_exception_title), + text = generalGetString(MR.strings.image_decoding_exception_desc) + ) + } + null + } + } else { + BitmapFactory.decodeFile(getAppFilePath(uri)) + }?.asImageBitmap() +} + +actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? { + return if (Build.VERSION.SDK_INT >= 31) { + val source = ImageDecoder.createSource(data) + try { + ImageDecoder.decodeBitmap(source) + } catch (e: android.graphics.ImageDecoder.DecodeException) { + Log.e(TAG, "Unable to decode the image: ${e.stackTraceToString()}") + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.image_decoding_exception_title), + text = generalGetString(MR.strings.image_decoding_exception_desc) + ) + } + null + } + } else { + BitmapFactory.decodeByteArray(data, 0, data.size) + }?.asImageBitmap() +} + +actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? { + return if (Build.VERSION.SDK_INT >= 28) { + val source = ImageDecoder.createSource(androidAppContext.contentResolver, uri.toUri()) + try { + ImageDecoder.decodeDrawable(source) + } catch (e: android.graphics.ImageDecoder.DecodeException) { + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.image_decoding_exception_title), + text = generalGetString(MR.strings.image_decoding_exception_desc) + ) + } + Log.e(TAG, "Error while decoding drawable: ${e.stackTraceToString()}") + null + } + } else { + Drawable.createFromPath(getAppFilePath(uri)) + } +} + +actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? { + return try { + val ext = if (asPng) "png" else "jpg" + tmpDir.mkdir() + return File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", ext)).apply { + outputStream().use { out -> + image.asAndroidBitmap().compress(if (asPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG, 85, out) + out.flush() + } + deleteOnExit() + ChatModel.filesToDelete.add(this) + } + } catch (e: Exception) { + Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}") + null + } +} + +actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { + val mmr = MediaMetadataRetriever() + mmr.setDataSource(androidAppContext, uri.toUri()) + val durationMs = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() + val image = when { + timestamp != null -> mmr.getFrameAtTime(timestamp * 1000, MediaMetadataRetriever.OPTION_CLOSEST) + random -> mmr.frameAtTime + else -> mmr.getFrameAtTime(0) + } + mmr.release() + return VideoPlayerInterface.PreviewAndDuration(image?.asImageBitmap(), durationMs, timestamp ?: 0) +} + +actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encodeToString(this, Base64.DEFAULT) + +actual fun String.toByteArrayFromBase64ForPassphrase(): ByteArray = Base64.decode(this, Base64.DEFAULT) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.android.kt similarity index 90% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.android.kt index 15ada1e103..dcb5055425 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ConnectViaLinkView.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.android.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -8,16 +8,11 @@ import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel +import chat.simplex.common.model.ChatModel import chat.simplex.res.MR -enum class ConnectViaLinkTab { - SCAN, PASTE -} - @Composable -fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) { +actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) { val selection = remember { mutableStateOf( runCatching { ConnectViaLinkTab.valueOf(m.controller.appPrefs.connectViaLinkTab.get()!!) }.getOrDefault(ConnectViaLinkTab.SCAN) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCode.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCode.android.kt new file mode 100644 index 0000000000..eee525827e --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCode.android.kt @@ -0,0 +1,18 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.ui.graphics.* + +actual fun ImageBitmap.replaceColor(from: Int, to: Int): ImageBitmap { + val pixels = IntArray(width * height) + val bitmap = this.asAndroidBitmap() + bitmap.getPixels(pixels, 0, width, 0, 0, width, height) + var i = 0 + while (i < pixels.size) { + if (pixels[i] == from) { + pixels[i] = to + } + i++ + } + bitmap.setPixels(pixels, 0, width, 0, 0, width, height) + return bitmap.asImageBitmap() +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt similarity index 96% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt rename to apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt index 4dcb4a0ade..7fb6445d57 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.android.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat import android.annotation.SuppressLint import android.util.Log @@ -18,14 +18,14 @@ import boofcv.alg.color.ColorFormat import boofcv.android.ConvertCameraImage import boofcv.factory.fiducial.FactoryFiducial import boofcv.struct.image.GrayU8 -import chat.simplex.app.TAG +import chat.simplex.common.platform.TAG import com.google.common.util.concurrent.ListenableFuture import java.util.concurrent.* // Adapted from learntodroid - https://gist.github.com/learntodroid/8f839be0b29d0378f843af70607bd7f5 @Composable -fun QRCodeScanner(onBarcode: (String) -> Unit) { +actual fun QRCodeScanner(onBarcode: (String) -> Unit) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current var preview by remember { mutableStateOf<Preview?>(null) } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.android.kt new file mode 100644 index 0000000000..d0cad31210 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.android.kt @@ -0,0 +1,20 @@ +package chat.simplex.common.views.newchat + +import android.Manifest +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import chat.simplex.common.model.ChatModel +import com.google.accompanist.permissions.rememberPermissionState + +@Composable +actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) + LaunchedEffect(Unit) { + cameraPermissionState.launchPermissionRequest() + } + ConnectContactLayout( + chatModel = chatModel, + incognitoPref = chatModel.controller.appPrefs.incognito, + close = close + ) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt new file mode 100644 index 0000000000..605c40445a --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.android.kt @@ -0,0 +1,26 @@ +package chat.simplex.common.views.onboarding + +import android.Manifest +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import chat.simplex.common.platform.ntfManager +import com.google.accompanist.permissions.rememberPermissionState + +@Composable +actual fun SetNotificationsModeAdditions() { + if (Build.VERSION.SDK_INT >= 33) { + val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) + LaunchedEffect(notificationsPermissionState.hasPermission) { + if (notificationsPermissionState.hasPermission) { + ntfManager.androidCreateNtfChannelsMaybeShowAlert() + } else { + notificationsPermissionState.launchPermissionRequest() + } + } + } else { + LaunchedEffect(Unit) { + ntfManager.androidCreateNtfChannelsMaybeShowAlert() + } + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt new file mode 100644 index 0000000000..e02c011fac --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -0,0 +1,169 @@ +package chat.simplex.common.views.usersettings + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionView +import android.app.Activity +import android.content.ComponentName +import android.content.pm.PackageManager +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import chat.simplex.common.R +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.helpers.APPLICATION_ID +import chat.simplex.common.helpers.saveAppLocale +import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +enum class AppIcon(val resId: Int) { + DEFAULT(R.drawable.icon_round_common), + DARK_BLUE(R.drawable.icon_dark_blue_round_common), +} + +@Composable +actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { + val appIcon = remember { mutableStateOf(findEnabledIcon()) } + + fun setAppIcon(newIcon: AppIcon) { + if (appIcon.value == newIcon) return + val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}") + val oldComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${appIcon.value.name.lowercase()}") + androidAppContext.packageManager.setComponentEnabledSetting( + newComponent, + COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP + ) + + androidAppContext.packageManager.setComponentEnabledSetting( + oldComponent, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP + ) + + appIcon.value = newIcon + } + + AppearanceScope.AppearanceLayout( + appIcon, + m.controller.appPrefs.appLanguage, + m.controller.appPrefs.systemDarkTheme, + changeIcon = ::setAppIcon, + showSettingsModal = showSettingsModal, + editColor = { name, initialColor -> + ModalManager.start.showModalCloseable { close -> + ColorEditor(name, initialColor, close) + } + }, + ) +} + +@Composable +fun AppearanceScope.AppearanceLayout( + icon: MutableState<AppIcon>, + languagePref: SharedPreference<String?>, + systemDarkTheme: SharedPreference<String?>, + changeIcon: (AppIcon) -> Unit, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + editColor: (ThemeColor, Color) -> Unit, +) { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + ) { + AppBarTitle(stringResource(MR.strings.appearance_settings)) + SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { + val context = LocalContext.current + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // SectionItemWithValue( + // generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, + // remember { mutableStateOf("system") }, + // listOf(ValueTitleDesc("system", generalGetString(MR.strings.change_verb), "")), + // onSelected = { openSystemLangPicker(context as? Activity ?: return@SectionItemWithValue) } + // ) + // } else { + val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } + LangSelector(state) { + state.value = it + withApi { + delay(200) + val activity = context as? Activity + if (activity != null) { + if (it == "system") { + activity.saveAppLocale(languagePref) + } else { + activity.saveAppLocale(languagePref, it) + } + } + } + } + // } + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { + LazyRow { + items(AppIcon.values().size, { index -> AppIcon.values()[index] }) { index -> + val item = AppIcon.values()[index] + val mipmap = ContextCompat.getDrawable(LocalContext.current, item.resId)!! + Image( + bitmap = mipmap.toBitmap().asImageBitmap(), + contentDescription = "", + contentScale = ContentScale.Fit, + modifier = Modifier + .shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondaryVariant) + .size(70.dp) + .clickable { changeIcon(item) } + .padding(10.dp) + ) + + if (index + 1 != AppIcon.values().size) { + Spacer(Modifier.padding(horizontal = 4.dp)) + } + } + } + } + + SectionDividerSpaced(maxTopPadding = true) + ThemesSection(systemDarkTheme, showSettingsModal, editColor) + SectionBottomSpacer() + } +} + +private fun findEnabledIcon(): AppIcon = AppIcon.values().first { icon -> + androidAppContext.packageManager.getComponentEnabledSetting( + ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${icon.name.lowercase()}") + ).let { it == COMPONENT_ENABLED_STATE_DEFAULT || it == COMPONENT_ENABLED_STATE_ENABLED } +} + +@Preview +@Composable +fun PreviewAppearanceSettings() { + SimpleXTheme { + AppearanceScope.AppearanceLayout( + icon = remember { mutableStateOf(AppIcon.DARK_BLUE) }, + languagePref = SharedPreference({ null }, {}), + systemDarkTheme = SharedPreference({ null }, {}), + changeIcon = {}, + showSettingsModal = { {} }, + editColor = { _, _ -> }, + ) + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.android.kt new file mode 100644 index 0000000000..3c1771c8b3 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.android.kt @@ -0,0 +1,32 @@ +package chat.simplex.common.views.usersettings + +import SectionView +import android.view.WindowManager +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.fragment.app.FragmentActivity +import chat.simplex.common.model.ChatModel +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun PrivacyDeviceSection( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + setPerformLA: (Boolean) -> Unit, +) { + SectionView(stringResource(MR.strings.settings_section_title_device)) { + ChatLockItem(showSettingsModal, setPerformLA) + val context = LocalContext.current + SettingsPreferenceItem(painterResource(MR.images.ic_visibility_off), stringResource(MR.strings.protect_app_screen), ChatModel.controller.appPrefs.privacyProtectScreen) { on -> + if (on) { + (context as? FragmentActivity)?.window?.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } else { + (context as? FragmentActivity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + } + } +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt new file mode 100644 index 0000000000..a1b7b3141d --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.android.kt @@ -0,0 +1,16 @@ +package chat.simplex.common.views.usersettings + +import android.Manifest +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import chat.simplex.common.model.ServerCfg +import com.google.accompanist.permissions.rememberPermissionState + +@Composable +actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) + LaunchedEffect(Unit) { + cameraPermissionState.launchPermissionRequest() + } + ScanProtocolServerLayout(onNext) +} diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt new file mode 100644 index 0000000000..49a29cf141 --- /dev/null +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -0,0 +1,49 @@ +package chat.simplex.common.views.usersettings + +import SectionView +import androidx.compose.runtime.Composable +import androidx.work.WorkManager +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import com.jakewharton.processphoenix.ProcessPhoenix +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun SettingsSectionApp( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), + showVersion: () -> Unit, + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +) { + SectionView(stringResource(MR.strings.settings_section_title_app)) { + SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) + AppVersionItem(showVersion) + } +} + + +private fun restartApp() { + ProcessPhoenix.triggerRebirth(androidAppContext) + shutdownApp() +} + +private fun shutdownApp() { + WorkManager.getInstance(androidAppContext).cancelAllWork() + platform.androidServiceSafeStop() + Runtime.getRuntime().exit(0) +} + +private fun shutdownAppAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.shutdown_alert_question), + text = generalGetString(MR.strings.shutdown_alert_desc), + destructive = true, + onConfirm = onConfirm + ) +} diff --git a/apps/multiplatform/android/src/main/res/drawable/edit_text_cursor.xml b/apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml similarity index 100% rename from apps/multiplatform/android/src/main/res/drawable/edit_text_cursor.xml rename to apps/multiplatform/common/src/androidMain/res/drawable/edit_text_cursor.xml 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 9a3c9c48ab..dbfee70257 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/android/simplex-api.c @@ -1,4 +1,8 @@ #include <jni.h> +#include <string.h> +#include <stdint.h> +//#include <stdlib.h> +//#include <android/log.h> // from the RTS void hs_init(int * argc, char **argv[]); @@ -19,7 +23,7 @@ extern void __rel_iplt_start(void){}; extern void reallocarray(void){}; JNIEXPORT jint JNICALL -Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { +Java_chat_simplex_common_platform_CoreKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE); int ret = pipe_std_to_socket(name); (*env)->ReleaseStringUTFChars(env, socket_name, name); @@ -27,7 +31,7 @@ Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jcla } JNIEXPORT void JNICALL -Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { +Java_chat_simplex_common_platform_CoreKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { hs_init(NULL, NULL); setLineBuffering(); } @@ -42,9 +46,13 @@ 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_password_hash(const char *pwd, const char *salt); +extern char *chat_write_file(const char *path, char *ptr, int length); +extern char *chat_read_file(const char *path, const char *key, const char *nonce); +extern char *chat_encrypt_file(const char *from_path, const char *to_path); +extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path); JNIEXPORT jobjectArray JNICALL -Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { +Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, __unused jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE); @@ -52,7 +60,7 @@ Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); - (*env)->ReleaseStringUTFChars(env, dbKey, _confirm); + (*env)->ReleaseStringUTFChars(env, confirm, _confirm); // Creating array of Object's (boxed values can be passed, eg. Long instead of long) jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); @@ -67,25 +75,28 @@ Java_chat_simplex_app_SimplexAppKt_chatMigrateInit(JNIEnv *env, __unused jclass } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); + //jint length = (jint) (*env)->GetStringUTFLength(env, msg); + //for (int i = 0; i < length; ++i) + // __android_log_print(ANDROID_LOG_ERROR, "simplex", "%d: %02x\n", i, _msg[i]); jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { +Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) { +Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, __unused jclass clazz, jlong controller, jint wait) { return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait)); } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) { +Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, __unused jclass clazz, jstring str) { const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str)); (*env)->ReleaseStringUTFChars(env, str, _str); @@ -93,7 +104,7 @@ Java_chat_simplex_app_SimplexAppKt_chatParseMarkdown(JNIEnv *env, __unused jclas } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatParseServer(JNIEnv *env, __unused jclass clazz, jstring str) { +Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, __unused jclass clazz, jstring str) { const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str)); (*env)->ReleaseStringUTFChars(env, str, _str); @@ -101,7 +112,7 @@ Java_chat_simplex_app_SimplexAppKt_chatParseServer(JNIEnv *env, __unused jclass } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatPasswordHash(JNIEnv *env, __unused jclass clazz, jstring pwd, jstring salt) { +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); const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE); jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt)); @@ -109,3 +120,76 @@ Java_chat_simplex_app_SimplexAppKt_chatPasswordHash(JNIEnv *env, __unused jclass (*env)->ReleaseStringUTFChars(env, salt, _salt); return res; } + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { + const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); + jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer); + jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer); + jstring res = (*env)->NewStringUTF(env, chat_write_file(_path, buff, capacity)); + (*env)->ReleaseStringUTFChars(env, path, _path); + return res; +} + +JNIEXPORT jobjectArray JNICALL +Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) { + const char *_path = (*env)->GetStringUTFChars(env, path, JNI_FALSE); + const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE); + const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE); + + jbyte *res = chat_read_file(_path, _key, _nonce); + (*env)->ReleaseStringUTFChars(env, path, _path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + + jint status = (jint)res[0]; + jbyteArray arr; + if (status == 0) { + union { + uint32_t w; + uint8_t b[4]; + } len; + len.b[0] = (uint8_t)res[1]; + len.b[1] = (uint8_t)res[2]; + len.b[2] = (uint8_t)res[3]; + len.b[3] = (uint8_t)res[4]; + arr = (*env)->NewByteArray(env, len.w); + (*env)->SetByteArrayRegion(env, arr, 0, len.w, res + 5); + } else { + int len = strlen(res + 1); // + 1 offset here is to not include status byte + arr = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, arr, 0, len, res + 1); + } + + jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); + jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"), + (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Integer"), "<init>", "(I)V"), + status); + (*env)->SetObjectArrayElement(env, ret, 0, statusObj); + (*env)->SetObjectArrayElement(env, ret, 1, arr); + return ret; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) { + const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE); + const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_encrypt_file(_from_path, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring key, jstring nonce, jstring to_path) { + const char *_from_path = (*env)->GetStringUTFChars(env, from_path, JNI_FALSE); + const char *_key = (*env)->GetStringUTFChars(env, key, JNI_FALSE); + const char *_nonce = (*env)->GetStringUTFChars(env, nonce, JNI_FALSE); + const char *_to_path = (*env)->GetStringUTFChars(env, to_path, JNI_FALSE); + jstring res = (*env)->NewStringUTF(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} diff --git a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt index 2f8f119d75..849a6c98a7 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/CMakeLists.txt @@ -19,6 +19,8 @@ project("app") if(UNIX AND NOT APPLE) set(OS_LIB_PATH "linux") set(OS_LIB_EXT "so") + # Makes ld search libs in the same dir as libapp-lib, not in system dirs + set(CMAKE_BUILD_RPATH "$ORIGIN") elseif(WIN32) set(OS_LIB_PATH "windows") set(OS_LIB_EXT "dll") @@ -37,9 +39,6 @@ else() set(OS_LIB_ARCH "${CMAKE_SYSTEM_PROCESSOR}") endif() -# Makes ld search libs in the same dir as libapp-lib, not in system dirs -#set(CMAKE_BUILD_RPATH "$ORIGIN") - # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. 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 e772adffa2..ddc5c92f93 100644 --- a/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c +++ b/apps/multiplatform/common/src/commonMain/cpp/desktop/simplex-api.c @@ -1,21 +1,13 @@ #include <jni.h> +#include <string.h> +#include <stdlib.h> +#include <stdint.h> // from the RTS void hs_init(int * argc, char **argv[]); -//extern void __svfscanf(void){}; -//extern void __vfwscanf(void){}; -//extern void __memset_chk_fail(void){}; -//extern void __strcpy_chk_generic(void){}; -//extern void __strcat_chk_generic(void){}; -//extern void __libc_globals(void){}; -//extern void __rel_iplt_start(void){}; - -// Android 9 only, not 13 -//extern void reallocarray(void){}; - JNIEXPORT void JNICALL -Java_chat_simplex_common_platform_BackendKt_initHS(JNIEnv *env, jclass clazz) { +Java_chat_simplex_common_platform_CoreKt_initHS(JNIEnv *env, jclass clazz) { hs_init(NULL, NULL); } @@ -29,14 +21,58 @@ 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_password_hash(const char *pwd, const char *salt); +extern char *chat_write_file(const char *path, char *ptr, int length); +extern char *chat_read_file(const char *path, const char *key, const char *nonce); +extern char *chat_encrypt_file(const char *from_path, const char *to_path); +extern char *chat_decrypt_file(const char *from_path, const char *key, const char *nonce, const char *to_path); + +// As a reference: https://stackoverflow.com/a/60002045 +jstring decode_to_utf8_string(JNIEnv *env, char *string) { + jobject bb = (*env)->NewDirectByteBuffer(env, (void *)string, strlen(string)); + jclass cls_charset = (*env)->FindClass(env, "java/nio/charset/Charset"); + jmethodID mid_charset_forName = (*env)->GetStaticMethodID(env, cls_charset, "forName", "(Ljava/lang/String;)Ljava/nio/charset/Charset;"); + jobject charset = (*env)->CallStaticObjectMethod(env, cls_charset, mid_charset_forName, (*env)->NewStringUTF(env, "UTF-8")); + + jmethodID mid_decode = (*env)->GetMethodID(env, cls_charset, "decode", "(Ljava/nio/ByteBuffer;)Ljava/nio/CharBuffer;"); + jobject cb = (*env)->CallObjectMethod(env, charset, mid_decode, bb); + + jclass cls_char_buffer = (*env)->FindClass(env, "java/nio/CharBuffer"); + jmethodID mid_to_string = (*env)->GetMethodID(env, cls_char_buffer, "toString", "()Ljava/lang/String;"); + jstring res = (*env)->CallObjectMethod(env, cb, mid_to_string); + + (*env)->DeleteLocalRef(env, bb); + (*env)->DeleteLocalRef(env, charset); + (*env)->DeleteLocalRef(env, cb); + return res; +} + +char * encode_to_utf8_chars(JNIEnv *env, jstring string) { + if (!string) return ""; + + const jclass cls_string = (*env)->FindClass(env, "java/lang/String"); + const jmethodID mid_getBytes = (*env)->GetMethodID(env, cls_string, "getBytes", "(Ljava/lang/String;)[B"); + const jbyteArray jbyte_array = (jbyteArray) (*env)->CallObjectMethod(env, string, mid_getBytes, (*env)->NewStringUTF(env, "UTF-8")); + jint length = (jint) (*env)->GetArrayLength(env, jbyte_array); + jbyte *jbytes = malloc(length + 1); + (*env)->GetByteArrayRegion(env, jbyte_array, 0, length, jbytes); + // char * should be null terminated but jbyte * isn't. Terminate it with \0. Otherwise, Haskell will not see the end of string + jbytes[length] = '\0'; + + //for (int i = 0; i < length; ++i) + // fprintf(stderr, "%d: %02x\n", i, jbytes[i]); + + (*env)->DeleteLocalRef(env, jbyte_array); + (*env)->DeleteLocalRef(env, cls_string); + return (char *) jbytes; +} JNIEXPORT jobjectArray JNICALL -Java_chat_simplex_common_platform_BackendKt_chatMigrateInit(JNIEnv *env, jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { - const char *_dbPath = (*env)->GetStringUTFChars(env, dbPath, JNI_FALSE); - const char *_dbKey = (*env)->GetStringUTFChars(env, dbKey, JNI_FALSE); - const char *_confirm = (*env)->GetStringUTFChars(env, confirm, JNI_FALSE); - jlong _ctrl = (jlong) 0; - jstring res = (*env)->NewStringUTF(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); +Java_chat_simplex_common_platform_CoreKt_chatMigrateInit(JNIEnv *env, jclass clazz, jstring dbPath, jstring dbKey, jstring confirm) { + const char *_dbPath = encode_to_utf8_chars(env, dbPath); + const char *_dbKey = encode_to_utf8_chars(env, dbKey); + const char *_confirm = encode_to_utf8_chars(env, confirm); + long int *_ctrl = (long) 0; + jstring res = decode_to_utf8_string(env, chat_migrate_init(_dbPath, _dbKey, _confirm, &_ctrl)); (*env)->ReleaseStringUTFChars(env, dbPath, _dbPath); (*env)->ReleaseStringUTFChars(env, dbKey, _dbKey); (*env)->ReleaseStringUTFChars(env, dbKey, _confirm); @@ -54,45 +90,118 @@ Java_chat_simplex_common_platform_BackendKt_chatMigrateInit(JNIEnv *env, jclass } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_BackendKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { - const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); +Java_chat_simplex_common_platform_CoreKt_chatSendCmd(JNIEnv *env, jclass clazz, jlong controller, jstring msg) { + const char *_msg = encode_to_utf8_chars(env, msg); + jstring res = decode_to_utf8_string(env, chat_send_cmd((void*)controller, _msg)); (*env)->ReleaseStringUTFChars(env, msg, _msg); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_BackendKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) { - return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); +Java_chat_simplex_common_platform_CoreKt_chatRecvMsg(JNIEnv *env, jclass clazz, jlong controller) { + return decode_to_utf8_string(env, chat_recv_msg((void*)controller)); } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_BackendKt_chatRecvMsgWait(JNIEnv *env, jclass clazz, jlong controller, jint wait) { - return (*env)->NewStringUTF(env, chat_recv_msg_wait((void*)controller, wait)); +Java_chat_simplex_common_platform_CoreKt_chatRecvMsgWait(JNIEnv *env, jclass clazz, jlong controller, jint wait) { + return decode_to_utf8_string(env, chat_recv_msg_wait((void*)controller, wait)); } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_BackendKt_chatParseMarkdown(JNIEnv *env, jclass clazz, jstring str) { - const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_parse_markdown(_str)); +Java_chat_simplex_common_platform_CoreKt_chatParseMarkdown(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = encode_to_utf8_chars(env, str); + jstring res = decode_to_utf8_string(env, chat_parse_markdown(_str)); (*env)->ReleaseStringUTFChars(env, str, _str); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_BackendKt_chatParseServer(JNIEnv *env, jclass clazz, jstring str) { - const char *_str = (*env)->GetStringUTFChars(env, str, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_parse_server(_str)); +Java_chat_simplex_common_platform_CoreKt_chatParseServer(JNIEnv *env, jclass clazz, jstring str) { + const char *_str = encode_to_utf8_chars(env, str); + jstring res = decode_to_utf8_string(env, chat_parse_server(_str)); (*env)->ReleaseStringUTFChars(env, str, _str); return res; } JNIEXPORT jstring JNICALL -Java_chat_simplex_common_platform_BackendKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) { - const char *_pwd = (*env)->GetStringUTFChars(env, pwd, JNI_FALSE); - const char *_salt = (*env)->GetStringUTFChars(env, salt, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_password_hash(_pwd, _salt)); +Java_chat_simplex_common_platform_CoreKt_chatPasswordHash(JNIEnv *env, jclass clazz, jstring pwd, jstring salt) { + const char *_pwd = encode_to_utf8_chars(env, pwd); + const char *_salt = encode_to_utf8_chars(env, salt); + jstring res = decode_to_utf8_string(env, chat_password_hash(_pwd, _salt)); (*env)->ReleaseStringUTFChars(env, pwd, _pwd); (*env)->ReleaseStringUTFChars(env, salt, _salt); return res; } + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatWriteFile(JNIEnv *env, jclass clazz, jstring path, jobject buffer) { + const char *_path = encode_to_utf8_chars(env, path); + jbyte *buff = (jbyte *) (*env)->GetDirectBufferAddress(env, buffer); + jlong capacity = (*env)->GetDirectBufferCapacity(env, buffer); + jstring res = decode_to_utf8_string(env, chat_write_file(_path, buff, capacity)); + (*env)->ReleaseStringUTFChars(env, path, _path); + return res; +} + +JNIEXPORT jobjectArray JNICALL +Java_chat_simplex_common_platform_CoreKt_chatReadFile(JNIEnv *env, jclass clazz, jstring path, jstring key, jstring nonce) { + const char *_path = encode_to_utf8_chars(env, path); + const char *_key = encode_to_utf8_chars(env, key); + const char *_nonce = encode_to_utf8_chars(env, nonce); + + jbyte *res = chat_read_file(_path, _key, _nonce); + (*env)->ReleaseStringUTFChars(env, path, _path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + + jint status = (jint)res[0]; + jbyteArray arr; + if (status == 0) { + union { + uint32_t w; + uint8_t b[4]; + } len; + len.b[0] = (uint8_t)res[1]; + len.b[1] = (uint8_t)res[2]; + len.b[2] = (uint8_t)res[3]; + len.b[3] = (uint8_t)res[4]; + arr = (*env)->NewByteArray(env, len.w); + (*env)->SetByteArrayRegion(env, arr, 0, len.w, res + 5); + } else { + int len = strlen(res + 1); // + 1 offset here is to not include status byte + arr = (*env)->NewByteArray(env, len); + (*env)->SetByteArrayRegion(env, arr, 0, len, res + 1); + } + + jobjectArray ret = (jobjectArray)(*env)->NewObjectArray(env, 2, (*env)->FindClass(env, "java/lang/Object"), NULL); + jobject statusObj = (*env)->NewObject(env, (*env)->FindClass(env, "java/lang/Integer"), + (*env)->GetMethodID(env, (*env)->FindClass(env, "java/lang/Integer"), "<init>", "(I)V"), + status); + (*env)->SetObjectArrayElement(env, ret, 0, statusObj); + (*env)->SetObjectArrayElement(env, ret, 1, arr); + return ret; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatEncryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring to_path) { + const char *_from_path = encode_to_utf8_chars(env, from_path); + const char *_to_path = encode_to_utf8_chars(env, to_path); + jstring res = decode_to_utf8_string(env, chat_encrypt_file(_from_path, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} + +JNIEXPORT jstring JNICALL +Java_chat_simplex_common_platform_CoreKt_chatDecryptFile(JNIEnv *env, jclass clazz, jstring from_path, jstring key, jstring nonce, jstring to_path) { + const char *_from_path = encode_to_utf8_chars(env, from_path); + const char *_key = encode_to_utf8_chars(env, key); + const char *_nonce = encode_to_utf8_chars(env, nonce); + const char *_to_path = encode_to_utf8_chars(env, to_path); + jstring res = decode_to_utf8_string(env, chat_decrypt_file(_from_path, _key, _nonce, _to_path)); + (*env)->ReleaseStringUTFChars(env, from_path, _from_path); + (*env)->ReleaseStringUTFChars(env, key, _key); + (*env)->ReleaseStringUTFChars(env, nonce, _nonce); + (*env)->ReleaseStringUTFChars(env, to_path, _to_path); + return res; +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt new file mode 100644 index 0000000000..c08ad5f914 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -0,0 +1,335 @@ +package chat.simplex.common + +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.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.dp +import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.SimpleButton +import chat.simplex.common.views.SplashView +import chat.simplex.common.views.call.ActiveCallView +import chat.simplex.common.views.call.IncomingCallAlertView +import chat.simplex.common.views.chat.ChatView +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.database.DatabaseErrorView +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.localauth.VerticalDivider +import chat.simplex.common.views.onboarding.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +data class SettingsViewState( + val userPickerState: MutableStateFlow<AnimatedViewState>, + val scaffoldState: ScaffoldState, + val switchingUsers: MutableState<Boolean> +) + +@Composable +fun AppScreen() { + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Surface(color = MaterialTheme.colors.background) { + MainScreen() + } + } +} + +@Composable +fun MainScreen() { + val chatModel = ChatModel + var showChatDatabaseError by rememberSaveable { + mutableStateOf(chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null) + } + LaunchedEffect(chatModel.chatDbStatus.value) { + showChatDatabaseError = chatModel.chatDbStatus.value != DBMigrationResult.OK && chatModel.chatDbStatus.value != null + } + var showAdvertiseLAAlert by remember { mutableStateOf(false) } + LaunchedEffect(showAdvertiseLAAlert) { + if ( + !chatModel.controller.appPrefs.laNoticeShown.get() + && showAdvertiseLAAlert + && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete + && chatModel.chats.isNotEmpty() + && chatModel.activeCallInvitation.value == null + ) { + AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) } + } + LaunchedEffect(chatModel.showAdvertiseLAUnavailableAlert.value) { + if (chatModel.showAdvertiseLAUnavailableAlert.value) { + laUnavailableInstructionAlert() + } + } + LaunchedEffect(chatModel.clearOverlays.value) { + if (chatModel.clearOverlays.value) { + ModalManager.closeAllModalsEverywhere() + chatModel.clearOverlays.value = false + } + } + + @Composable + fun AuthView() { + Surface(color = MaterialTheme.colors.background) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + SimpleButton( + stringResource(MR.strings.auth_unlock), + icon = painterResource(MR.images.ic_lock), + click = { + AppLock.laFailed.value = false + AppLock.runAuthenticate() + } + ) + } + } + } + + Box { + var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) } + LaunchedEffect(Unit) { + snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it } + } + val userCreated = chatModel.userCreated.value + var showInitializationView by remember { mutableStateOf(false) } + when { + chatModel.chatDbStatus.value == null && showInitializationView -> InitializationView() + showChatDatabaseError -> { + chatModel.chatDbStatus.value?.let { + DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs) + } + } + remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView() + onboarding == OnboardingStage.OnboardingComplete && userCreated -> { + Box { + showAdvertiseLAAlert = true + val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } + val scaffoldState = rememberScaffoldState() + val switchingUsers = rememberSaveable { mutableStateOf(false) } + val settingsState = remember { SettingsViewState(userPickerState, scaffoldState, switchingUsers) } + if (appPlatform.isAndroid) { + AndroidScreen(settingsState) + } else { + DesktopScreen(settingsState) + } + } + } + onboarding == OnboardingStage.Step1_SimpleXInfo -> { + SimpleXInfo(chatModel, onboarding = true) + if (appPlatform.isDesktop) { + ModalManager.fullscreen.showInView() + } + } + onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {} + onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) + onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel) + onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) + } + if (appPlatform.isAndroid) { + ModalManager.fullscreen.showInView() + } + + val unauthorized = remember { derivedStateOf { AppLock.userAuthorized.value != true } } + if (unauthorized.value && !(chatModel.activeCallViewIsVisible.value && chatModel.showCallView.value)) { + LaunchedEffect(Unit) { + // With these constrains when user presses back button while on ChatList, activity destroys and shows auth request + // while the screen moves to a launcher. Detect it and prevent showing the auth + if (!(AppLock.destroyedAfterBackPress.value && chatModel.controller.appPrefs.laMode.get() == LAMode.SYSTEM)) { + AppLock.runAuthenticate() + } + } + if (chatModel.controller.appPrefs.performLA.get() && AppLock.laFailed.value) { + AuthView() + } else { + SplashView() + } + } else if (chatModel.showCallView.value) { + ActiveCallView() + } + ModalManager.fullscreen.showPasscodeInView() + val invitation = chatModel.activeCallInvitation.value + if (invitation != null) IncomingCallAlertView(invitation, chatModel) + AlertManager.shared.showInView() + + LaunchedEffect(Unit) { + delay(1000) + if (chatModel.chatDbStatus.value == null) { + showInitializationView = true + } + } + } + + DisposableEffectOnRotate { + // When using lock delay = 0 and screen rotates, the app will be locked which is not useful. + // Let's prolong the unlocked period to 3 sec for screen rotation to take place + if (chatModel.controller.appPrefs.laLockDelay.get() == 0) { + AppLock.enteredBackground.value = AppLock.elapsedRealtime() + 3000 + } + } +} + +@Composable +fun AndroidScreen(settingsState: SettingsViewState) { + BoxWithConstraints { + var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } + val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } + Box( + Modifier + .graphicsLayer { + translationX = -offset.value.dp.toPx() + } + ) { + StartPartOfScreen(settingsState) + } + val scope = rememberCoroutineScope() + val onComposed: suspend (chatId: String?) -> Unit = { chatId -> + // coroutine, scope and join() because: + // - it should be run from coroutine to wait until this function finishes + // - without using scope.launch it throws CancellationException when changing user + // - join allows to wait until completion + scope.launch { + offset.animateTo( + if (chatId == null) 0f else maxWidth.value, + chatListAnimationSpec() + ) + }.join() + } + LaunchedEffect(Unit) { + launch { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .collect { + if (it == null) onComposed(null) + currentChatId = it + } + } + } + Box(Modifier.graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() }) Box2@{ + currentChatId?.let { + ChatView(it, chatModel, onComposed) + } + } + } +} + +@Composable +fun StartPartOfScreen(settingsState: SettingsViewState) { + if (chatModel.setDeliveryReceipts.value) { + SetDeliveryReceiptsView(chatModel) + } else { + val stopped = chatModel.chatRunning.value == false + if (chatModel.sharedContent.value == null) + ChatListView(chatModel, settingsState, AppLock::setPerformLA, stopped) + else + ShareListView(chatModel, settingsState, stopped) + } +} + +@Composable +fun CenterPartOfScreen() { + val currentChatId by remember { ChatModel.chatId } + LaunchedEffect(Unit) { + snapshotFlow { currentChatId } + .distinctUntilChanged() + .collect { + if (it != null) { + ModalManager.center.closeModals() + } + } + } + when (val id = currentChatId) { + null -> { + if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { + Box( + Modifier + .fillMaxSize() + .background(MaterialTheme.colors.background), + contentAlignment = Alignment.Center + ) { + Text(stringResource(MR.strings.no_selected_chat)) + } + } else { + ModalManager.center.showInView() + } + } + else -> ChatView(id, chatModel) {} + } +} + +@Composable +fun EndPartOfScreen() { + ModalManager.end.showInView() +} + +@Composable +fun DesktopScreen(settingsState: SettingsViewState) { + Box { + // 56.dp is a size of unused space of settings drawer + Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) { + StartPartOfScreen(settingsState) + } + Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) { + ModalManager.start.showInView() + } + Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) { + Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { + CenterPartOfScreen() + } + if (ModalManager.end.hasModalsOpen()) { + VerticalDivider() + } + Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) { + EndPartOfScreen() + } + } + val (userPickerState, scaffoldState, switchingUsers ) = settingsState + val scope = rememberCoroutineScope() + if (scaffoldState.drawerState.isOpen) { + Box( + Modifier + .fillMaxSize() + .padding(start = DEFAULT_START_MODAL_WIDTH) + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { + ModalManager.start.closeModals() + scope.launch { settingsState.scaffoldState.drawerState.close() } + }) + ) + } + VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) + UserPicker(chatModel, userPickerState, switchingUsers) { + scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + } + ModalManager.fullscreen.showInView() + } +} + +@Composable +fun InitializationView() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator( + Modifier + .padding(bottom = DEFAULT_PADDING) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) + Text(stringResource(MR.strings.opening_database)) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt new file mode 100644 index 0000000000..7228f4ebf4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/AppLock.kt @@ -0,0 +1,276 @@ +package chat.simplex.common + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Modifier +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.localauth.SetAppPasscodeView +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import kotlinx.coroutines.* + +object AppLock { + /** + * We don't want these values to be bound to Activity lifecycle since activities are changed often, for example, when a user + * clicks on new message in notification. In this case savedInstanceState will be null (this prevents restoring the values) + * See [SimplexService.onTaskRemoved] for another part of the logic which nullifies the values when app closed by the user + * */ + val userAuthorized = mutableStateOf<Boolean?>(null) + val enteredBackground = mutableStateOf<Long?>(null) + + // Remember result and show it after orientation change + val laFailed = mutableStateOf(false) + val destroyedAfterBackPress = mutableStateOf(false) + + fun clearAuthState() { + userAuthorized.value = null + enteredBackground.value = null + } + + fun showLANotice(laNoticeShown: SharedPreference<Boolean>) { + Log.d(TAG, "showLANotice") + if (!laNoticeShown.get()) { + laNoticeShown.set(true) + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.la_notice_title_simplex_lock), + text = generalGetString(MR.strings.la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled), + confirmText = generalGetString(MR.strings.la_notice_turn_on), + onConfirm = { + laNoticeShown.set(true) + withBGApi { // to remove this call, change ordering of onConfirm call in AlertManager + if (appPlatform.isAndroid) { + showChooseLAMode() + } else { + AlertManager.shared.hideAlert() + setPasscode() + } + } + }, + onDismiss = { + AlertManager.shared.hideAlert() + } + ) + } + } + + private fun showChooseLAMode() { + Log.d(TAG, "showLANotice") + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.la_lock_mode), + text = null, + confirmText = generalGetString(MR.strings.la_lock_mode_passcode), + dismissText = generalGetString(MR.strings.la_lock_mode_system), + onConfirm = { + AlertManager.shared.hideAlert() + setPasscode() + }, + onDismiss = { + AlertManager.shared.hideAlert() + initialEnableLA() + } + ) + } + + private fun initialEnableLA() { + val m = ChatModel + val appPrefs = ChatController.appPrefs + appPrefs.laMode.set(LAMode.default) + authenticate( + generalGetString(MR.strings.auth_enable_simplex_lock), + generalGetString(MR.strings.auth_confirm_credential), + completed = { laResult -> + when (laResult) { + LAResult.Success -> { + m.performLA.value = true + appPrefs.performLA.set(true) + laTurnedOnAlert() + } + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { + m.performLA.value = false + appPrefs.performLA.set(false) + laFailedAlert() + } + is LAResult.Unavailable -> { + m.performLA.value = false + appPrefs.performLA.set(false) + m.showAdvertiseLAUnavailableAlert.value = true + } + } + } + ) + } + + private fun setPasscode() { + val appPrefs = ChatController.appPrefs + ModalManager.fullscreen.showCustomModal { close -> + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + SetAppPasscodeView( + submit = { + ChatModel.performLA.value = true + appPrefs.performLA.set(true) + appPrefs.laMode.set(LAMode.PASSCODE) + laTurnedOnAlert() + }, + cancel = { + ChatModel.performLA.value = false + appPrefs.performLA.set(false) + laPasscodeNotSetAlert() + }, + close = close + ) + } + } + } + + fun setAuthState() { + userAuthorized.value = !ChatController.appPrefs.performLA.get() + } + + fun runAuthenticate() { + val m = ChatModel + setAuthState() + if (userAuthorized.value == false) { + // To make Main thread free in order to allow to Compose to show blank view that hiding content underneath of it faster on slow devices + CoroutineScope(Dispatchers.Default).launch { + delay(50) + withContext(Dispatchers.Main) { + authenticate( + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(MR.strings.auth_unlock) + else + generalGetString(MR.strings.la_enter_app_passcode), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(MR.strings.auth_log_in_using_credential) + else + generalGetString(MR.strings.auth_unlock), + selfDestruct = true, + completed = { laResult -> + when (laResult) { + LAResult.Success -> + userAuthorized.value = true + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { + laFailed.value = true + if (m.controller.appPrefs.laMode.get() == LAMode.PASSCODE) { + laFailedAlert() + } + } + is LAResult.Unavailable -> { + userAuthorized.value = true + m.performLA.value = false + m.controller.appPrefs.performLA.set(false) + laUnavailableTurningOffAlert() + } + } + } + ) + } + } + } + } + + fun setPerformLA(on: Boolean) { + ChatController.appPrefs.laNoticeShown.set(true) + if (on) { + enableLA() + } else { + disableLA() + } + } + + private fun enableLA() { + val m = ChatModel + authenticate( + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(MR.strings.auth_enable_simplex_lock) + else + generalGetString(MR.strings.new_passcode), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(MR.strings.auth_confirm_credential) + else + "", + completed = { laResult -> + val prefPerformLA = m.controller.appPrefs.performLA + when (laResult) { + LAResult.Success -> { + m.performLA.value = true + prefPerformLA.set(true) + laTurnedOnAlert() + } + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { + m.performLA.value = false + prefPerformLA.set(false) + laFailedAlert() + } + is LAResult.Unavailable -> { + m.performLA.value = false + prefPerformLA.set(false) + laUnavailableInstructionAlert() + } + } + } + ) + } + + private fun disableLA() { + val m = ChatModel + authenticate( + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(MR.strings.auth_disable_simplex_lock) + else + generalGetString(MR.strings.la_enter_app_passcode), + if (m.controller.appPrefs.laMode.get() == LAMode.SYSTEM) + generalGetString(MR.strings.auth_confirm_credential) + else + generalGetString(MR.strings.auth_disable_simplex_lock), + completed = { laResult -> + val prefPerformLA = m.controller.appPrefs.performLA + val selfDestructPref = m.controller.appPrefs.selfDestruct + when (laResult) { + LAResult.Success -> { + m.performLA.value = false + prefPerformLA.set(false) + DatabaseUtils.ksAppPassword.remove() + selfDestructPref.set(false) + DatabaseUtils.ksSelfDestructPassword.remove() + } + is LAResult.Failed -> { /* Can be called multiple times on every failure */ } + is LAResult.Error -> { + m.performLA.value = true + prefPerformLA.set(true) + laFailedAlert() + } + is LAResult.Unavailable -> { + m.performLA.value = false + prefPerformLA.set(false) + laUnavailableTurningOffAlert() + } + } + } + ) + } + + fun elapsedRealtime(): Long = System.nanoTime() / 1_000_000 + + fun recheckAuthState() { + val enteredBackgroundVal = enteredBackground.value + val delay = ChatController.appPrefs.laLockDelay.get() + if (enteredBackgroundVal == null || elapsedRealtime() - enteredBackgroundVal >= delay * 1000) { + if (userAuthorized.value != false) { + /** [runAuthenticate] will be called in [MainScreen] if needed. Making like this prevents double showing of passcode on start */ + setAuthState() + } else if (!ChatModel.activeCallViewIsVisible.value) { + runAuthenticate() + } + } + } + fun appWasHidden() { + enteredBackground.value = elapsedRealtime() + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 5adc17fc61..887abe756b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1,19 +1,18 @@ -package chat.simplex.app.model +package chat.simplex.common.model -import android.net.Uri import androidx.compose.material.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.call.* -import chat.simplex.app.views.chat.ComposeState -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.OnboardingStage -import chat.simplex.app.views.usersettings.NotificationPreviewMode -import chat.simplex.app.views.usersettings.NotificationsMode +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.common.views.chat.ComposeState +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @@ -26,6 +25,7 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* import java.io.File +import java.net.URI import java.time.format.DateTimeFormatter import java.time.format.FormatStyle import java.util.* @@ -38,7 +38,6 @@ import kotlin.time.* @Stable object ChatModel { val controller: ChatController = ChatController - val onboardingStage = mutableStateOf<OnboardingStage?>(null) val setDeliveryReceipts = mutableStateOf(false) val currentUser = mutableStateOf<User?>(null) val users = mutableStateListOf<UserInfo>() @@ -66,14 +65,21 @@ object ChatModel { val clearOverlays = mutableStateOf<Boolean>(false) // set when app is opened via contact or invitation URI - val appOpenUrl = mutableStateOf<Uri?>(null) + val appOpenUrl = mutableStateOf<URI?>(null) // preferences - val notificationsMode by lazy { mutableStateOf(NotificationsMode.values().firstOrNull { it.name == controller.appPrefs.notificationsMode.get() } ?: NotificationsMode.default) } - val notificationPreviewMode by lazy { mutableStateOf(NotificationPreviewMode.values().firstOrNull { it.name == controller.appPrefs.notificationPreviewMode.get() } ?: NotificationPreviewMode.default) } - val performLA by lazy { mutableStateOf(controller.appPrefs.performLA.get()) } + val notificationPreviewMode by lazy { + mutableStateOf( + try { + NotificationPreviewMode.valueOf(controller.appPrefs.notificationPreviewMode.get()!!) + } catch (e: Exception) { + NotificationPreviewMode.default + } + ) + } + val performLA by lazy { mutableStateOf(ChatController.appPrefs.performLA.get()) } val showAdvertiseLAUnavailableAlert = mutableStateOf(false) - val incognito by lazy { mutableStateOf(controller.appPrefs.incognito.get()) } + val showChatPreviews by lazy { mutableStateOf(ChatController.appPrefs.privacyShowChatPreviews.get()) } // current WebRTC call val callManager = CallManager(this) @@ -95,7 +101,7 @@ object ChatModel { val sharedContent = mutableStateOf(null as SharedContent?) val filesToDelete = mutableSetOf<File>() - val simplexLinkMode by lazy { mutableStateOf(controller.appPrefs.simplexLinkMode.get()) } + val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) } fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value @@ -123,10 +129,11 @@ object ChatModel { } } - fun hasChat(id: String): Boolean = chats.firstOrNull { it.id == id } != null - fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } - fun getContactChat(contactId: Long): Chat? = chats.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } - private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } + // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens + fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null + fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id } + fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } + private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id } fun addChat(chat: Chat) = chats.add(index = 0, chat) fun updateChatInfo(cInfo: ChatInfo) { @@ -399,18 +406,18 @@ object ChatModel { ) } - fun increaseUnreadCounter(user: User) { + fun increaseUnreadCounter(user: UserLike) { changeUnreadCounter(user, 1) } - fun decreaseUnreadCounter(user: User, by: Int = 1) { + fun decreaseUnreadCounter(user: UserLike, by: Int = 1) { changeUnreadCounter(user, -by) } - private fun changeUnreadCounter(user: User, by: Int) { + private fun changeUnreadCounter(user: UserLike, by: Int) { val i = users.indexOfFirst { it.user.userId == user.userId } if (i != -1) { - users[i] = UserInfo(user, users[i].unreadCount + by) + users[i] = UserInfo(users[i].user, users[i].unreadCount + by) } } @@ -430,7 +437,7 @@ object ChatModel { val info = getChat(id)?.chatInfo as? ChatInfo.ContactConnection ?: return if (info.contactConnection.connReqInv == connReqInv.value) { connReqInv.value = null - ModalManager.shared.closeModals() + ModalManager.center.closeModals() } } @@ -492,17 +499,17 @@ enum class ChatType(val type: String) { @Serializable data class User( - val userId: Long, + override val userId: Long, val userContactId: Long, val localDisplayName: String, val profile: LocalProfile, val fullPreferences: FullChatPreferences, - val activeUser: Boolean, - val showNtfs: Boolean, + override val activeUser: Boolean, + override val showNtfs: Boolean, val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, val viewPwdHash: UserPwdHash? -): NamedChat { +): NamedChat, UserLike { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName override val image: String? get() = profile.image @@ -510,8 +517,6 @@ data class User( val hidden: Boolean = viewPwdHash != null - val showNotifications: Boolean = activeUser || showNtfs - val addressShared: Boolean = profile.contactLink != null companion object { @@ -530,6 +535,22 @@ data class User( } } +@Serializable +data class UserRef( + override val userId: Long, + val localDisplayName: String, + override val activeUser: Boolean, + override val showNtfs: Boolean +): UserLike {} + +interface UserLike { + val userId: Long + val activeUser: Boolean + val showNtfs: Boolean + + val showNotifications: Boolean get() = activeUser || showNtfs +} + @Serializable data class UserPwdHash( val hash: String, @@ -584,10 +605,13 @@ data class Chat ( val userCanSend: Boolean get() = when (chatInfo) { is ChatInfo.Direct -> true - is ChatInfo.Group -> { - val m = chatInfo.groupInfo.membership - m.memberActive && m.memberRole >= GroupMemberRole.Member - } + is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member + else -> false + } + + val nextSendGrpInv: Boolean + get() = when (chatInfo) { + is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv else -> false } @@ -777,13 +801,18 @@ data class Contact( val userPreferences: ChatPreferences, val mergedPreferences: ContactUserPreferences, override val createdAt: Instant, - override val updatedAt: Instant + override val updatedAt: Instant, + val contactGroupMemberId: Long? = null, + val contactGrpInvSent: Boolean ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" override val apiId get() = contactId override val ready get() = activeConn.connStatus == ConnStatus.Ready - override val sendMsgEnabled get() = !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false) + override val sendMsgEnabled get() = + (ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false)) + || nextSendGrpInv + val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent override val ntfsEnabled get() = chatSettings.enableNtfs override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { @@ -834,7 +863,8 @@ data class Contact( userPreferences = ChatPreferences.sampleData, mergedPreferences = ContactUserPreferences.sampleData, createdAt = Clock.System.now(), - updatedAt = Clock.System.now() + updatedAt = Clock.System.now(), + contactGrpInvSent = false ) } } @@ -859,6 +889,7 @@ class ContactSubStatus( data class Connection( val connId: Long, val agentConnId: String, + val peerChatVRange: VersionRange, val connStatus: ConnStatus, val connLevel: Int, val viaGroupLink: Boolean, @@ -868,10 +899,17 @@ data class Connection( ) { val id: ChatId get() = ":$connId" companion object { - val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null) + val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null) } } +@Serializable +data class VersionRange(val minVersion: Int, val maxVersion: Int) { + + fun isCompatibleRange(vRange: VersionRange): Boolean = + this.minVersion <= vRange.maxVersion && vRange.minVersion <= this.maxVersion +} + @Serializable data class SecurityCode(val securityCode: String, val verifiedAt: Instant) @@ -924,6 +962,14 @@ data class LocalProfile( } } +@Serializable +data class UserProfileUpdateSummary( + val notChanged: Int, + val updateSuccesses: Int, + val updateFailures: Int, + val changedContacts: List<Contact> +) + @Serializable class Group ( val groupInfo: GroupInfo, @@ -1194,6 +1240,7 @@ class MemberSubError ( @Serializable class UserContactRequest ( val contactRequestId: Long, + val cReqChatVRange: VersionRange, override val localDisplayName: String, val profile: Profile, override val createdAt: Instant, @@ -1216,6 +1263,7 @@ class UserContactRequest ( companion object { val sampleData = UserContactRequest( contactRequestId = 1, + cReqChatVRange = VersionRange(1, 1), localDisplayName = "alice", profile = Profile.sampleData, createdAt = Clock.System.now(), @@ -1365,6 +1413,12 @@ data class ChatItem ( private val isLiveDummy: Boolean get() = meta.itemId == TEMP_LIVE_CHAT_ITEM_ID + val encryptedFile: Boolean? = if (file?.fileSource == null) null else file.fileSource.cryptoArgs != null + + val encryptLocalFile: Boolean + get() = content.msgContent !is MsgContent.MCVideo && + chatController.appPrefs.privacyEncryptLocalFiles.get() + val memberDisplayName: String? get() = if (chatDir is CIDirection.GroupRcv) chatDir.groupMember.displayName else null @@ -1378,6 +1432,18 @@ data class ChatItem ( else -> false } + val memberConnected: GroupMember? get() = + when (chatDir) { + is CIDirection.GroupRcv -> when (content) { + is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) { + is RcvGroupEvent.MemberConnected -> chatDir.groupMember + else -> null + } + else -> null + } + else -> null + } + fun memberToModerate(chatInfo: ChatInfo): Pair<GroupInfo, GroupMember>? { return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) { val m = chatInfo.groupInfo.membership @@ -1416,6 +1482,7 @@ data class ChatItem ( is RcvGroupEvent.GroupDeleted -> showNtfDir is RcvGroupEvent.GroupUpdated -> false is RcvGroupEvent.InvitedViaGroupLink -> false + is RcvGroupEvent.MemberCreatedContact -> false } is CIContent.SndGroupEventContent -> showNtfDir is CIContent.RcvConnEventContent -> false @@ -1616,19 +1683,12 @@ data class CIMeta ( val isRcvNew: Boolean get() = itemStatus is CIStatus.RcvNew - fun statusIcon(primaryColor: Color, metaColor: Color = CurrentColors.value.colors.secondary): Pair<ImageResource, Color>? = - when (itemStatus) { - is CIStatus.SndSent -> MR.images.ic_check_filled to metaColor - is CIStatus.SndRcvd -> when(itemStatus.msgRcptStatus) { - MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor - MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red - } - is CIStatus.SndErrorAuth -> MR.images.ic_close to Color.Red - is CIStatus.SndError -> MR.images.ic_warning_filled to WarningYellow - is CIStatus.RcvNew -> MR.images.ic_circle_filled to primaryColor - is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor - else -> null - } + fun statusIcon( + primaryColor: Color, + metaColor: Color = CurrentColors.value.colors.secondary, + paleMetaColor: Color = CurrentColors.value.colors.secondary + ): Pair<ImageResource, Color>? = + itemStatus.statusIcon(primaryColor, metaColor, paleMetaColor) companion object { fun getSample( @@ -1707,13 +1767,49 @@ fun localTimestamp(t: Instant): String { @Serializable sealed class CIStatus { @Serializable @SerialName("sndNew") class SndNew: CIStatus() - @Serializable @SerialName("sndSent") class SndSent: CIStatus() - @Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus): CIStatus() + @Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus() + @Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus() @Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus() @Serializable @SerialName("rcvNew") class RcvNew: CIStatus() @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() + + fun statusIcon( + primaryColor: Color, + metaColor: Color = CurrentColors.value.colors.secondary, + paleMetaColor: Color = CurrentColors.value.colors.secondary + ): Pair<ImageResource, Color>? = + when (this) { + is SndNew -> null + is SndSent -> when (this.sndProgress) { + SndCIStatusProgress.Complete -> MR.images.ic_check_filled to metaColor + SndCIStatusProgress.Partial -> MR.images.ic_check_filled to paleMetaColor + } + is SndRcvd -> when(this.msgRcptStatus) { + MsgReceiptStatus.Ok -> when (this.sndProgress) { + SndCIStatusProgress.Complete -> MR.images.ic_double_check to metaColor + SndCIStatusProgress.Partial -> MR.images.ic_double_check to paleMetaColor + } + MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red + } + is SndErrorAuth -> MR.images.ic_close to Color.Red + is SndError -> MR.images.ic_warning_filled to WarningYellow + is RcvNew -> MR.images.ic_circle_filled to primaryColor + is RcvRead -> null + is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor + } + + val statusInto: Pair<String, String>? get() = when (this) { + is SndNew -> null + is SndSent -> null + is SndRcvd -> null + is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc) + is SndError -> generalGetString(MR.strings.message_delivery_error_title) to (generalGetString(MR.strings.unknown_error) + ": $agentError") + is RcvNew -> null + is RcvRead -> null + is Invalid -> "Invalid status" to this.text + } } @Serializable @@ -1722,6 +1818,12 @@ enum class MsgReceiptStatus { @SerialName("badMsgHash") BadMsgHash; } +@Serializable +enum class SndCIStatusProgress { + @SerialName("partial") Partial, + @SerialName("complete") Complete; +} + @Serializable sealed class CIDeleted { @Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted() @@ -1796,6 +1898,19 @@ sealed class CIContent: ItemContent { is InvalidJSON -> "invalid data" } + val showMemberName: Boolean get() = + when (this) { + is RcvMsgContent -> true + is RcvDeleted -> true + is RcvCall -> true + is RcvIntegrityError -> true + is RcvDecryptionError -> true + is RcvGroupInvitation -> true + is RcvModerated -> true + is InvalidJSON -> true + else -> false + } + companion object { fun featureText(feature: Feature, enabled: String, param: Int?): String = if (feature.hasParam) { @@ -1935,7 +2050,7 @@ class CIFile( val fileId: Long, val fileName: String, val fileSize: Long, - val filePath: String? = null, + val fileSource: CryptoFile? = null, val fileStatus: CIFileStatus, val fileProtocol: FileProtocol ) { @@ -1983,10 +2098,44 @@ class CIFile( filePath: String? = "test.txt", fileStatus: CIFileStatus = CIFileStatus.RcvComplete ): CIFile = - CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, filePath = filePath, fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP) + CIFile(fileId = fileId, fileName = fileName, fileSize = fileSize, fileSource = if (filePath == null) null else CryptoFile.plain(filePath), fileStatus = fileStatus, fileProtocol = FileProtocol.XFTP) } } +@Serializable +data class CryptoFile( + val filePath: String, + val cryptoArgs: CryptoFileArgs? +) { + + val isAbsolutePath: Boolean + get() = File(filePath).isAbsolute + + @Transient + private var tmpFile: File? = null + + fun createTmpFileIfNeeded(): File { + if (tmpFile == null) { + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + ChatModel.filesToDelete.add(tmpFile) + this.tmpFile = tmpFile + } + return tmpFile!! + } + + fun deleteTmpFile() { + tmpFile?.delete() + } + + companion object { + fun plain(f: String): CryptoFile = CryptoFile(f, null) + } +} + +@Serializable +data class CryptoFileArgs(val fileKey: String, val fileNonce: String) + class CancelAction( val uiActionId: StringResource, val alert: AlertInfo @@ -2350,6 +2499,7 @@ sealed class RcvGroupEvent() { @Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent() @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent() @Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent() + @Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent() val text: String get() = when (this) { is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName) @@ -2362,6 +2512,7 @@ sealed class RcvGroupEvent() { is GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted) is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile) is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link) + is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact) } } @@ -2485,6 +2636,7 @@ sealed class ChatItemTTL: Comparable<ChatItemTTL?> { @Serializable class ChatItemInfo( val itemVersions: List<ChatItemVersion>, + val memberDeliveryStatuses: List<MemberDeliveryStatus>? ) @Serializable @@ -2495,3 +2647,17 @@ data class ChatItemVersion( val itemVersionTs: Instant, val createdAt: Instant, ) + +@Serializable +data class MemberDeliveryStatus( + val groupMemberId: Long, + val memberDeliveryStatus: CIStatus +) + +enum class NotificationPreviewMode { + MESSAGE, CONTACT, HIDDEN; + + companion object { + val default: NotificationPreviewMode = MESSAGE + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt new file mode 100644 index 0000000000..037d27af33 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/CryptoFile.kt @@ -0,0 +1,59 @@ +package chat.simplex.common.model + +import chat.simplex.common.platform.* +import kotlinx.serialization.* +import java.nio.ByteBuffer + +@Serializable +sealed class WriteFileResult { + @Serializable @SerialName("result") data class Result(val cryptoArgs: CryptoFileArgs): WriteFileResult() + @Serializable @SerialName("error") data class Error(val writeError: String): WriteFileResult() +} + +/* + fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { + val str = chatWriteFile(path, data) + return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) { + is WriteFileResult.Result -> d.cryptoArgs + is WriteFileResult.Error -> throw Exception(d.writeError) + } +} +* */ + +fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs { + val buffer = ByteBuffer.allocateDirect(data.size) + buffer.put(data) + buffer.rewind() + val str = chatWriteFile(path, buffer) + return when (val d = json.decodeFromString(WriteFileResult.serializer(), str)) { + is WriteFileResult.Result -> d.cryptoArgs + is WriteFileResult.Error -> throw Exception(d.writeError) + } +} + +fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray { + val res: Array<Any> = chatReadFile(path, cryptoArgs.fileKey, cryptoArgs.fileNonce) + val status = (res[0] as Integer).toInt() + val arr = res[1] as ByteArray + if (status == 0) { + return arr + } else { + throw Exception(String(arr)) + } +} + +fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs { + val str = chatEncryptFile(fromPath, toPath) + val d = json.decodeFromString(WriteFileResult.serializer(), str) + return when (d) { + is WriteFileResult.Result -> d.cryptoArgs + is WriteFileResult.Error -> throw Exception(d.writeError) + } +} + +fun decryptCryptoFile(fromPath: String, cryptoArgs: CryptoFileArgs, toPath: String) { + val err = chatDecryptFile(fromPath, cryptoArgs.fileKey, cryptoArgs.fileNonce, toPath) + if (err != "") { + throw Exception(err) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt similarity index 79% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/model/SimpleXAPI.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 448c0e2f90..060738bc18 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1,21 +1,20 @@ -package chat.simplex.app.model +package chat.simplex.common.model -import android.content.* -import android.util.Log -import chat.simplex.app.views.helpers.* +import chat.simplex.common.views.helpers.* import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource -import chat.simplex.app.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.call.* -import chat.simplex.app.views.newchat.ConnectViaLinkTab -import chat.simplex.app.views.onboarding.OnboardingStage -import chat.simplex.app.views.usersettings.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.common.views.newchat.ConnectViaLinkTab +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.common.views.usersettings.* import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import chat.simplex.res.MR +import com.russhwolf.settings.Settings import kotlinx.coroutines.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -27,6 +26,12 @@ import java.util.Date typealias ChatCtrl = Long +// currentChatVersion in core +const val CURRENT_CHAT_VERSION: Int = 2 + +// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) +val CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion = 2, maxVersion = CURRENT_CHAT_VERSION) + enum class CallOnLockScreen { DISABLE, SHOW, @@ -43,19 +48,17 @@ enum class SimplexLinkMode { BROWSER; companion object { - val default = SimplexLinkMode.DESCRIPTION + val default = DESCRIPTION } } class AppPreferences { - private val sharedPreferences: SharedPreferences = SimplexApp.context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE) - private val sharedPreferencesThemes: SharedPreferences = SimplexApp.context.getSharedPreferences(SHARED_PREFS_THEMES_ID, Context.MODE_PRIVATE) - // deprecated, remove in 2024 private val runServiceInBackground = mkBoolPreference(SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND, true) - val notificationsMode = mkStrPreference(SHARED_PREFS_NOTIFICATIONS_MODE, - if (!runServiceInBackground.get()) NotificationsMode.OFF.name else NotificationsMode.default.name - ) + val notificationsMode = mkEnumPreference( + SHARED_PREFS_NOTIFICATIONS_MODE, + if (!runServiceInBackground.get()) NotificationsMode.OFF else NotificationsMode.default + ) { NotificationsMode.values().firstOrNull { it.name == this } } val notificationPreviewMode = mkStrPreference(SHARED_PREFS_NOTIFICATION_PREVIEW_MODE, NotificationPreviewMode.default.name) val backgroundServiceNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_NOTICE_SHOWN, false) val backgroundServiceBatteryNoticeShown = mkBoolPreference(SHARED_PREFS_SERVICE_BATTERY_NOTICE_SHOWN, false) @@ -74,7 +77,7 @@ class AppPreferences { set = fun(action: CallOnLockScreen) { _callOnLockScreen.set(action.name) } ) val performLA = mkBoolPreference(SHARED_PREFS_PERFORM_LA, false) - val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.SYSTEM) { LAMode.values().firstOrNull { it.name == this } } + val laMode = mkEnumPreference(SHARED_PREFS_LA_MODE, LAMode.default) { LAMode.values().firstOrNull { it.name == this } } val laLockDelay = mkIntPreference(SHARED_PREFS_LA_LOCK_DELAY, 30) val laNoticeShown = mkBoolPreference(SHARED_PREFS_LA_NOTICE_SHOWN, false) val webrtcIceServers = mkStrPreference(SHARED_PREFS_WEBRTC_ICE_SERVERS, null) @@ -93,7 +96,10 @@ class AppPreferences { }, set = fun(mode: SimplexLinkMode) { _simplexLinkMode.set(mode.name) } ) + val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) + val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) + val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -141,7 +147,7 @@ class AppPreferences { val initializationVectorAppPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_APP_PASSPHRASE, null) val encryptedSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE, null) val initializationVectorSelfDestructPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE, null) - val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true) + val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null) val confirmDBUpgrades = mkBoolPreference(SHARED_PREFS_CONFIRM_DB_UPGRADES, false) val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) @@ -152,7 +158,7 @@ class AppPreferences { json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) }, decode = { json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) - }, sharedPreferencesThemes) + }, settingsThemes) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) @@ -160,65 +166,73 @@ class AppPreferences { private fun mkIntPreference(prefName: String, default: Int) = SharedPreference( - get = fun() = sharedPreferences.getInt(prefName, default), - set = fun(value) = sharedPreferences.edit().putInt(prefName, value).apply() + get = fun() = settings.getInt(prefName, default), + set = fun(value) = settings.putInt(prefName, value) ) private fun mkLongPreference(prefName: String, default: Long) = SharedPreference( - get = fun() = sharedPreferences.getLong(prefName, default), - set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply() + get = fun() = settings.getLong(prefName, default), + set = fun(value) = settings.putLong(prefName, value) ) private fun mkTimeoutPreference(prefName: String, default: Long, proxyDefault: Long): SharedPreference<Long> { val d = if (networkUseSocksProxy.get()) proxyDefault else default return SharedPreference( - get = fun() = sharedPreferences.getLong(prefName, d), - set = fun(value) = sharedPreferences.edit().putLong(prefName, value).apply() + get = fun() = settings.getLong(prefName, d), + set = fun(value) = settings.putLong(prefName, value) ) } private fun mkBoolPreference(prefName: String, default: Boolean) = SharedPreference( - get = fun() = sharedPreferences.getBoolean(prefName, default), - set = fun(value) = sharedPreferences.edit().putBoolean(prefName, value).apply() + get = fun() = settings.getBoolean(prefName, default), + set = fun(value) = settings.putBoolean(prefName, value) ) private fun mkStrPreference(prefName: String, default: String?): SharedPreference<String?> = SharedPreference( - get = fun() = sharedPreferences.getString(prefName, default), - set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply() + get = { + val nullValue = "----------------------" + val pref = settings.getString(prefName, default ?: nullValue) + if (pref != nullValue) { + pref + } else { + null + } + }, + set = fun(value) = if (value != null) settings.putString(prefName, value) else settings.remove(prefName) ) private fun <T> mkEnumPreference(prefName: String, default: T, construct: String.() -> T?): SharedPreference<T> = SharedPreference( - get = fun() = sharedPreferences.getString(prefName, default.toString())?.construct() ?: default, - set = fun(value) = sharedPreferences.edit().putString(prefName, value.toString()).apply() + get = fun() = settings.getString(prefName, default.toString()).construct() ?: default, + set = fun(value) = settings.putString(prefName, value.toString()) ) - /** - * Provide `[commit] = true` to save preferences right now, not after some unknown period of time. - * So in case of a crash this value will be saved 100% - * */ - private fun mkDatePreference(prefName: String, default: Instant?, commit: Boolean = false): SharedPreference<Instant?> = + // LALAL + private fun mkDatePreference(prefName: String, default: Instant?): SharedPreference<Instant?> = SharedPreference( get = { - val pref = sharedPreferences.getString(prefName, default?.toEpochMilliseconds()?.toString()) - pref?.let { Instant.fromEpochMilliseconds(pref.toLong()) } + val nullValue = "----------------------" + val pref = settings.getString(prefName, default?.toEpochMilliseconds()?.toString() ?: nullValue) + if (pref != nullValue) { + Instant.fromEpochMilliseconds(pref.toLong()) + } else { + null + } }, - set = fun(value) = sharedPreferences.edit().putString(prefName, value?.toEpochMilliseconds()?.toString()).let { - if (commit) it.commit() else it.apply() - } + set = fun(value) = if (value?.toEpochMilliseconds() != null) settings.putString(prefName, value.toEpochMilliseconds().toString()) else settings.remove(prefName) ) - private fun <K, V> mkMapPreference(prefName: String, default: Map<K, V>, encode: (Map<K, V>) -> String, decode: (String) -> Map<K, V>, prefs: SharedPreferences = sharedPreferences): SharedPreference<Map<K,V>> = + private fun <K, V> mkMapPreference(prefName: String, default: Map<K, V>, encode: (Map<K, V>) -> String, decode: (String) -> Map<K, V>, prefs: Settings = settings): SharedPreference<Map<K,V>> = SharedPreference( - get = fun() = decode(prefs.getString(prefName, encode(default))!!), - set = fun(value) = prefs.edit().putString(prefName, encode(value)).apply() + get = fun() = decode(prefs.getString(prefName, encode(default))), + set = fun(value) = prefs.putString(prefName, encode(value)) ) companion object { - internal const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" + const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES" private const val SHARED_PREFS_AUTO_RESTART_WORKER_VERSION = "AutoRestartWorkerVersion" private const val SHARED_PREFS_RUN_SERVICE_IN_BACKGROUND = "RunServiceInBackground" @@ -238,8 +252,11 @@ class AppPreferences { 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_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" + private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" + private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" - internal const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" + private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" + const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" @@ -293,7 +310,6 @@ private const val MESSAGE_TIMEOUT: Int = 15_000_000 object ChatController { var ctrl: ChatCtrl? = -1 val appPrefs: AppPreferences by lazy { AppPreferences() } - val ntfManager by lazy { NtfManager } val chatModel = ChatModel private var receiverStarted = false @@ -315,8 +331,8 @@ object ChatController { try { if (chatModel.chatRunning.value == true) return apiSetNetworkConfig(getNetCfg()) - apiSetTempFolder(getTempFilesDirectory()) - apiSetFilesFolder(getAppFilesDirectory()) + apiSetTempFolder(coreTmpDir.absolutePath) + apiSetFilesFolder(appFilesDir.absolutePath) apiSetXFTPConfig(getXFTPCfg()) val justStarted = apiStartChat() val users = listUsers() @@ -325,9 +341,8 @@ object ChatController { if (justStarted) { chatModel.currentUser.value = user chatModel.userCreated.value = true - apiSetIncognito(chatModel.incognito.value) getUserChatData() - chatModel.controller.appPrefs.chatLastStart.set(Clock.System.now()) + appPrefs.chatLastStart.set(Clock.System.now()) chatModel.chatRunning.value = true startReceiver() Log.d(TAG, "startChat: started") @@ -396,19 +411,15 @@ object ChatController { return withContext(Dispatchers.IO) { val c = cmd.cmdString - if (cmd !is CC.ApiParseMarkdown) { - chatModel.addTerminalItem(TerminalItem.cmd(cmd.obfuscated)) - Log.d(TAG, "sendCmd: ${cmd.cmdType}") - } + chatModel.addTerminalItem(TerminalItem.cmd(cmd.obfuscated)) + Log.d(TAG, "sendCmd: ${cmd.cmdType}") val json = chatSendCmd(ctrl, c) val r = APIResponse.decodeStr(json) Log.d(TAG, "sendCmd response type ${r.resp.responseType}") if (r.resp is CR.Response || r.resp is CR.Invalid) { Log.d(TAG, "sendCmd response json $json") } - if (r.resp !is CR.ParsedMarkdown) { - chatModel.addTerminalItem(TerminalItem.resp(r.resp)) - } + chatModel.addTerminalItem(TerminalItem.resp(r.resp)) r.resp } } @@ -465,13 +476,19 @@ object ChatController { suspend fun apiSetAllContactReceipts(enable: Boolean) { val r = sendCmd(CC.SetAllContactReceipts(enable)) if (r is CR.CmdOk) return - throw Exception("failed to enable receipts for all users ${r.responseType} ${r.details}") + throw Exception("failed to set receipts for all users ${r.responseType} ${r.details}") } suspend fun apiSetUserContactReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) { val r = sendCmd(CC.ApiSetUserContactReceipts(userId, userMsgReceiptSettings)) if (r is CR.CmdOk) return - throw Exception("failed to enable receipts for user contacts ${r.responseType} ${r.details}") + throw Exception("failed to set receipts for user contacts ${r.responseType} ${r.details}") + } + + suspend fun apiSetUserGroupReceipts(userId: Long, userMsgReceiptSettings: UserMsgReceiptSettings) { + val r = sendCmd(CC.ApiSetUserGroupReceipts(userId, userMsgReceiptSettings)) + if (r is CR.CmdOk) return + throw Exception("failed to set receipts for user groups ${r.responseType} ${r.details}") } suspend fun apiHideUser(userId: Long, viewPwd: String): User = @@ -534,12 +551,6 @@ object ChatController { throw Error("apiSetXFTPConfig bad response: ${r.responseType} ${r.details}") } - suspend fun apiSetIncognito(incognito: Boolean) { - val r = sendCmd(CC.SetIncognito(incognito)) - if (r is CR.CmdOk) return - throw Exception("failed to set incognito: ${r.responseType} ${r.details}") - } - suspend fun apiExportArchive(config: ArchiveConfig) { val r = sendCmd(CC.ApiExportArchive(config)) if (r is CR.CmdOk) return @@ -582,7 +593,7 @@ object ChatController { return null } - suspend fun apiSendMessage(type: ChatType, id: Long, file: String? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? { + suspend fun apiSendMessage(type: ChatType, id: Long, file: CryptoFile? = null, quotedItemId: Long? = null, mc: MsgContent, live: Boolean = false, ttl: Int? = null): AChatItem? { val cmd = CC.ApiSendMessage(type, id, file, quotedItemId, mc, live, ttl) val r = sendCmd(cmd) return when (r) { @@ -779,16 +790,18 @@ object ChatController { return null } - suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> { + suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String>? { val r = sendCmd(CC.APIGetContactCode(contactId)) if (r is CR.ContactCode) return r.contact to r.connectionCode - throw Exception("failed to get contact code: ${r.responseType} ${r.details}") + Log.e(TAG,"failed to get contact code: ${r.responseType} ${r.details}") + return null } - suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair<GroupMember, String> { + suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair<GroupMember, String>? { val r = sendCmd(CC.APIGetGroupMemberCode(groupId, groupMemberId)) if (r is CR.GroupMemberCode) return r.member to r.connectionCode - throw Exception("failed to get group member code: ${r.responseType} ${r.details}") + Log.e(TAG,"failed to get group member code: ${r.responseType} ${r.details}") + return null } suspend fun apiVerifyContact(contactId: Long, connectionCode: String?): Pair<Boolean, String>? { @@ -807,14 +820,14 @@ object ChatController { - suspend fun apiAddContact(): String? { + suspend fun apiAddContact(incognito: Boolean): Pair<String, PendingContactConnection>? { val userId = chatModel.currentUser.value?.userId ?: run { Log.e(TAG, "apiAddContact: no current user") return null } - val r = sendCmd(CC.APIAddContact(userId)) + val r = sendCmd(CC.APIAddContact(userId, incognito)) return when (r) { - is CR.Invitation -> r.connReqInvitation + is CR.Invitation -> r.connReqInvitation to r.connection else -> { if (!(networkErrorAlert(r))) { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) @@ -824,12 +837,19 @@ object ChatController { } } - suspend fun apiConnect(connReq: String): Boolean { + suspend fun apiSetConnectionIncognito(connId: Long, incognito: Boolean): PendingContactConnection? { + val r = sendCmd(CC.ApiSetConnectionIncognito(connId, incognito)) + if (r is CR.ConnectionIncognitoUpdated) return r.toConnection + Log.e(TAG, "apiSetConnectionIncognito bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiConnect(incognito: Boolean, connReq: String): Boolean { val userId = chatModel.currentUser.value?.userId ?: run { Log.e(TAG, "apiConnect: no current user") return false } - val r = sendCmd(CC.APIConnect(userId, connReq)) + val r = sendCmd(CC.APIConnect(userId, incognito, connReq)) when { r is CR.SentConfirmation || r is CR.SentInvitation -> return true r is CR.ContactAlreadyExists -> { @@ -899,11 +919,11 @@ object ChatController { return null } - suspend fun apiUpdateProfile(profile: Profile): Profile? { + suspend fun apiUpdateProfile(profile: Profile): Pair<Profile, List<Contact>>? { val userId = kotlin.runCatching { currentUserId("apiUpdateProfile") }.getOrElse { return null } val r = sendCmd(CC.ApiUpdateProfile(userId, profile)) - if (r is CR.UserProfileNoChange) return profile - if (r is CR.UserProfileUpdated) return r.toProfile + if (r is CR.UserProfileNoChange) return profile to emptyList() + if (r is CR.UserProfileUpdated) return r.toProfile to r.updateSummary.changedContacts Log.e(TAG, "apiUpdateProfile bad response: ${r.responseType} ${r.details}") return null } @@ -965,7 +985,8 @@ object ChatController { val r = sendCmd(CC.ApiShowMyAddress(userId)) if (r is CR.UserContactLink) return r.contactLink if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore - && r.chatError.storeError is StoreError.UserContactLinkNotFound) { + && r.chatError.storeError is StoreError.UserContactLinkNotFound + ) { return null } Log.e(TAG, "apiGetUserAddress bad response: ${r.responseType} ${r.details}") @@ -977,15 +998,16 @@ object ChatController { val r = sendCmd(CC.ApiAddressAutoAccept(userId, autoAccept)) if (r is CR.UserContactLinkUpdated) return r.contactLink if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore - && r.chatError.storeError is StoreError.UserContactLinkNotFound) { + && r.chatError.storeError is StoreError.UserContactLinkNotFound + ) { return null } Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? { - val r = sendCmd(CC.ApiAcceptContact(contactReqId)) + suspend fun apiAcceptContactRequest(incognito: Boolean, contactReqId: Long): Contact? { + val r = sendCmd(CC.ApiAcceptContact(incognito, contactReqId)) return when { r is CR.AcceptingContactRequest -> r.contact r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent @@ -1066,23 +1088,26 @@ object ChatController { return false } - suspend fun apiReceiveFile(fileId: Long, inline: Boolean? = null): AChatItem? { - val r = sendCmd(CC.ReceiveFile(fileId, inline)) + suspend fun apiReceiveFile(fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { + val r = sendCmd(CC.ReceiveFile(fileId, encrypted, inline)) return when (r) { is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAcceptedSndCancelled -> { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.cannot_receive_file), - generalGetString(MR.strings.sender_cancelled_file_transfer) - ) + Log.d(TAG, "apiReceiveFile error: sender cancelled file transfer") + if (!auto) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.cannot_receive_file), + generalGetString(MR.strings.sender_cancelled_file_transfer) + ) + } null } + else -> { if (!(networkErrorAlert(r))) { - if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat - && r.chatError.errorType is ChatErrorType.FileAlreadyReceiving - ) { - Log.d(TAG, "apiReceiveFile ignoring FileAlreadyReceiving error") + val maybeChatError = chatError(r) + if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { + Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") } else { apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) } @@ -1255,6 +1280,30 @@ object ChatController { } } + suspend fun apiCreateMemberContact(groupId: Long, groupMemberId: Long): Contact? { + return when (val r = sendCmd(CC.APICreateMemberContact(groupId, groupMemberId))) { + is CR.NewMemberContact -> r.contact + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r) + } + null + } + } + } + + suspend fun apiSendMemberContactInvitation(contactId: Long, mc: MsgContent): Contact? { + return when (val r = sendCmd(CC.APISendMemberContactInvitation(contactId, mc))) { + is CR.NewMemberContactSentInv -> r.contact + else -> { + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r) + } + null + } + } + } + suspend fun allowFeatureToContact(contact: Contact, feature: ChatFeature, param: Int? = null) { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(contact.contactId, prefs) @@ -1273,13 +1322,6 @@ object ChatController { } } - suspend fun apiParseMarkdown(text: String): List<FormattedText>? { - val r = sendCmd(CC.ApiParseMarkdown(text)) - if (r is CR.ParsedMarkdown) return r.formattedText - Log.e(TAG, "apiParseMarkdown bad response: ${r.responseType} ${r.details}") - return null - } - private fun networkErrorAlert(r: CR): Boolean { return when { r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent @@ -1404,9 +1446,9 @@ object ChatController { ((mc is MsgContent.MCImage && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV) || (mc is MsgContent.MCVideo && file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV) || (mc is MsgContent.MCVoice && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileStatus !is CIFileStatus.RcvAccepted))) { - withApi { receiveFile(r.user, file.fileId) } + withApi { receiveFile(r.user, file.fileId, encrypted = cItem.encryptLocalFile && chatController.appPrefs.privacyEncryptLocalFiles.get(), auto = true) } } - if (cItem.showNotification && (!SimplexApp.context.isAppOnForeground || chatModel.chatId.value != cInfo.id)) { + if (cItem.showNotification && (allowedToShowNotification() || chatModel.chatId.value != cInfo.id)) { ntfManager.notifyMessageReceived(r.user, cInfo, cItem) } } @@ -1517,6 +1559,10 @@ object ChatController { if (active(r.user)) { chatModel.updateGroup(r.toGroup) } + is CR.NewMemberContactReceivedInv -> + if (active(r.user)) { + chatModel.updateContact(r.contact) + } is CR.RcvFileStart -> chatItemSimpleUpdate(r.user, r.chatItem) is CR.RcvFileComplete -> @@ -1559,7 +1605,7 @@ object ChatController { // TODO check encryption is compatible withCall(r, r.contact) { call -> chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey) - val useRelay = chatModel.controller.appPrefs.webrtcPolicyRelay.get() + val useRelay = appPrefs.webrtcPolicyRelay.get() val iceServers = getIceServers() Log.d(TAG, ".callOffer iceServers $iceServers") chatModel.callCommand.value = WCallCommand.Offer( @@ -1627,7 +1673,7 @@ object ChatController { } } - private fun active(user: User): Boolean = user.userId == chatModel.currentUser.value?.userId + private fun active(user: UserLike): Boolean = user.userId == chatModel.currentUser.value?.userId private fun withCall(r: CR, contact: Contact, perform: (Call) -> Unit) { val call = chatModel.activeCall.value @@ -1638,8 +1684,8 @@ object ChatController { } } - suspend fun receiveFile(user: User, fileId: Long) { - val chatItem = apiReceiveFile(fileId) + suspend fun receiveFile(user: UserLike, fileId: Long, encrypted: Boolean, auto: Boolean = false) { + val chatItem = apiReceiveFile(fileId, encrypted = encrypted, auto = auto) if (chatItem != null) { chatItemSimpleUpdate(user, chatItem) } @@ -1652,7 +1698,7 @@ object ChatController { } } - private suspend fun chatItemSimpleUpdate(user: User, aChatItem: AChatItem) { + private suspend fun chatItemSimpleUpdate(user: UserLike, aChatItem: AChatItem) { val cInfo = aChatItem.chatInfo val cItem = aChatItem.chatItem val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) } @@ -1777,6 +1823,7 @@ sealed class CC { class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC() class SetAllContactReceipts(val enable: Boolean): CC() class ApiSetUserContactReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() + class ApiSetUserGroupReceipts(val userId: Long, val userMsgReceiptSettings: UserMsgReceiptSettings): CC() class ApiHideUser(val userId: Long, val viewPwd: String): CC() class ApiUnhideUser(val userId: Long, val viewPwd: String): CC() class ApiMuteUser(val userId: Long): CC() @@ -1787,7 +1834,6 @@ sealed class CC { class SetTempFolder(val tempFolder: String): CC() class SetFilesFolder(val filesFolder: String): CC() class ApiSetXFTPConfig(val config: XFTPFileConfig?): CC() - class SetIncognito(val incognito: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() class ApiDeleteStorage: CC() @@ -1795,7 +1841,7 @@ sealed class CC { class ApiGetChats(val userId: Long): CC() class ApiGetChat(val type: ChatType, val id: Long, val pagination: ChatPagination, val search: String = ""): CC() class ApiGetChatItemInfo(val type: ChatType, val id: Long, val itemId: Long): CC() - class ApiSendMessage(val type: ChatType, val id: Long, val file: String?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() + class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC() class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC() @@ -1812,6 +1858,8 @@ sealed class CC { class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIDeleteGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC() + class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC() + class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC() class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC() class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC() class APITestProtoServer(val userId: Long, val server: String): CC() @@ -1832,14 +1880,14 @@ sealed class CC { class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC() class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC() class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() - class APIAddContact(val userId: Long): CC() - class APIConnect(val userId: Long, val connReq: String): CC() + class APIAddContact(val userId: Long, val incognito: Boolean): CC() + class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() + class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() class ApiDeleteChat(val type: ChatType, val id: Long): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiListContacts(val userId: Long): CC() class ApiUpdateProfile(val userId: Long, val profile: Profile): CC() class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC() - class ApiParseMarkdown(val text: String): CC() class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() class ApiCreateMyAddress(val userId: Long): CC() @@ -1854,11 +1902,11 @@ sealed class CC { class ApiSendCallExtraInfo(val contact: Contact, val extraInfo: WebRTCExtraInfo): CC() class ApiEndCall(val contact: Contact): CC() class ApiCallStatus(val contact: Contact, val callStatus: WebRTCCallStatus): CC() - class ApiAcceptContact(val contactReqId: Long): CC() + class ApiAcceptContact(val incognito: Boolean, val contactReqId: Long): CC() class ApiRejectContact(val contactReqId: Long): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() - class ReceiveFile(val fileId: Long, val inline: Boolean?): CC() + class ReceiveFile(val fileId: Long, val encrypted: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() class ShowVersion(): CC() @@ -1874,7 +1922,11 @@ sealed class CC { is SetAllContactReceipts -> "/set receipts all ${onOff(enable)}" is ApiSetUserContactReceipts -> { val mrs = userMsgReceiptSettings - "/_set receipts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}" + "/_set receipts contacts $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}" + } + is ApiSetUserGroupReceipts -> { + val mrs = userMsgReceiptSettings + "/_set receipts groups $userId ${onOff(mrs.enable)} clear_overrides=${onOff(mrs.clearOverrides)}" } is ApiHideUser -> "/_hide user $userId ${json.encodeToString(viewPwd)}" is ApiUnhideUser -> "/_unhide user $userId ${json.encodeToString(viewPwd)}" @@ -1886,7 +1938,6 @@ sealed class CC { is SetTempFolder -> "/_temp_folder $tempFolder" is SetFilesFolder -> "/_files_folder $filesFolder" is ApiSetXFTPConfig -> if (config != null) "/_xftp on ${json.encodeToString(config)}" else "/_xftp off" - is SetIncognito -> "/incognito ${onOff(incognito)}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" is ApiDeleteStorage -> "/_db delete" @@ -1914,6 +1965,8 @@ sealed class CC { is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIDeleteGroupLink -> "/_delete link #$groupId" is APIGetGroupLink -> "/_get link #$groupId" + is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId" + is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}" is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}" is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}" is APITestProtoServer -> "/_server test $userId $server" @@ -1934,14 +1987,14 @@ sealed class CC { is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId" is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else "" is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" - is APIAddContact -> "/_connect $userId" - is APIConnect -> "/_connect $userId $connReq" + is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" + is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" + is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" - is ApiParseMarkdown -> "/_parse $text" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" is ApiCreateMyAddress -> "/_address $userId" @@ -1949,7 +2002,7 @@ sealed class CC { is ApiShowMyAddress -> "/_show_address $userId" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" is ApiAddressAutoAccept -> "/_auto_accept $userId ${AutoAccept.cmdString(autoAccept)}" - is ApiAcceptContact -> "/_accept $contactReqId" + is ApiAcceptContact -> "/_accept incognito=${onOff(incognito)} $contactReqId" is ApiRejectContact -> "/_reject $contactReqId" is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}" is ApiRejectCall -> "/_call reject @${contact.apiId}" @@ -1960,7 +2013,7 @@ sealed class CC { is ApiCallStatus -> "/_call status @${contact.apiId} ${callStatus.value}" is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" - is ReceiveFile -> if (inline == null) "/freceive $fileId" else "/freceive $fileId inline=${onOff(inline)}" + is ReceiveFile -> "/freceive $fileId encrypt=${onOff(encrypted)}" + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" is ShowVersion -> "/version" } @@ -1973,6 +2026,7 @@ sealed class CC { is ApiSetActiveUser -> "apiSetActiveUser" is SetAllContactReceipts -> "setAllContactReceipts" is ApiSetUserContactReceipts -> "apiSetUserContactReceipts" + is ApiSetUserGroupReceipts -> "apiSetUserGroupReceipts" is ApiHideUser -> "apiHideUser" is ApiUnhideUser -> "apiUnhideUser" is ApiMuteUser -> "apiMuteUser" @@ -1983,7 +2037,6 @@ sealed class CC { is SetTempFolder -> "setTempFolder" is SetFilesFolder -> "setFilesFolder" is ApiSetXFTPConfig -> "apiSetXFTPConfig" - is SetIncognito -> "setIncognito" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" is ApiDeleteStorage -> "apiDeleteStorage" @@ -2008,6 +2061,8 @@ sealed class CC { is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIGetGroupLink -> "apiGetGroupLink" + is APICreateMemberContact -> "apiCreateMemberContact" + is APISendMemberContactInvitation -> "apiSendMemberContactInvitation" is APIGetUserProtoServers -> "apiGetUserProtoServers" is APISetUserProtoServers -> "apiSetUserProtoServers" is APITestProtoServer -> "testProtoServer" @@ -2029,13 +2084,13 @@ sealed class CC { is APIVerifyContact -> "apiVerifyContact" is APIVerifyGroupMember -> "apiVerifyGroupMember" is APIAddContact -> "apiAddContact" + is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" is APIConnect -> "apiConnect" is ApiDeleteChat -> "apiDeleteChat" is ApiClearChat -> "apiClearChat" is ApiListContacts -> "apiListContacts" is ApiUpdateProfile -> "apiUpdateProfile" is ApiSetContactPrefs -> "apiSetContactPrefs" - is ApiParseMarkdown -> "apiParseMarkdown" is ApiSetContactAlias -> "apiSetContactAlias" is ApiSetConnectionAlias -> "apiSetConnectionAlias" is ApiCreateMyAddress -> "apiCreateMyAddress" @@ -2122,7 +2177,7 @@ sealed class ChatPagination { } @Serializable -class ComposedMessage(val filePath: String?, val quotedItemId: Long?, val msgContent: MsgContent) +class ComposedMessage(val fileSource: CryptoFile?, val quotedItemId: Long?, val msgContent: MsgContent) @Serializable class XFTPFileConfig(val minFileSize: Long) @@ -2337,7 +2392,7 @@ data class NetCfg( sessionMode = TransportSessionMode.User, tcpConnectTimeout = 15_000_000, tcpTimeout = 10_000_000, - tcpTimeoutPerKb = 20_000, + tcpTimeoutPerKb = 30_000, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 @@ -2351,7 +2406,7 @@ data class NetCfg( sessionMode = TransportSessionMode.User, tcpConnectTimeout = 30_000_000, tcpTimeout = 20_000_000, - tcpTimeoutPerKb = 40_000, + tcpTimeoutPerKb = 60_000, tcpKeepAlive = KeepAliveOpts.defaults, smpPingInterval = 1200_000_000, smpPingCount = 3 @@ -3137,7 +3192,7 @@ class APIResponse(val resp: CR, val corr: String? = null) { val type = resp["type"]?.jsonPrimitive?.content ?: "invalid" try { if (type == "apiChats") { - val user: User = json.decodeFromJsonElement(resp["user"]!!.jsonObject) + val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject) val chats: List<Chat> = resp["chats"]!!.jsonArray.map { parseChatData(it) } @@ -3146,7 +3201,7 @@ class APIResponse(val resp: CR, val corr: String? = null) { corr = data["corr"]?.toString() ) } else if (type == "apiChat") { - val user: User = json.decodeFromJsonElement(resp["user"]!!.jsonObject) + val user: UserRef = json.decodeFromJsonElement(resp["user"]!!.jsonObject) val chat = parseChatData(resp["chat"]!!) return APIResponse( resp = CR.ApiChat(user, chat), @@ -3154,11 +3209,18 @@ class APIResponse(val resp: CR, val corr: String? = null) { ) } else if (type == "chatCmdError") { val userObject = resp["user_"]?.jsonObject - val user = runCatching<User?> { json.decodeFromJsonElement(userObject!!) }.getOrNull() + val user = runCatching<UserRef?> { json.decodeFromJsonElement(userObject!!) }.getOrNull() return APIResponse( resp = CR.ChatCmdError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), corr = data["corr"]?.toString() ) + } else if (type == "chatError") { + val userObject = resp["user_"]?.jsonObject + val user = runCatching<UserRef?> { json.decodeFromJsonElement(userObject!!) }.getOrNull() + return APIResponse( + resp = CR.ChatRespError(user, ChatError.ChatErrorInvalidJSON(json.encodeToString(resp["chatError"]))), + corr = data["corr"]?.toString() + ) } } catch (e: Exception) { Log.e(TAG, "Error while parsing chat(s): " + e.stackTraceToString()) @@ -3202,122 +3264,127 @@ sealed class CR { @Serializable @SerialName("chatStarted") class ChatStarted: CR() @Serializable @SerialName("chatRunning") class ChatRunning: CR() @Serializable @SerialName("chatStopped") class ChatStopped: CR() - @Serializable @SerialName("apiChats") class ApiChats(val user: User, val chats: List<Chat>): CR() - @Serializable @SerialName("apiChat") class ApiChat(val user: User, val chat: Chat): CR() - @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: User, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() - @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: User, val servers: UserProtocolServers): CR() - @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: User, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() - @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: User, val chatItemTTL: Long? = null): CR() + @Serializable @SerialName("apiChats") class ApiChats(val user: UserRef, val chats: List<Chat>): CR() + @Serializable @SerialName("apiChat") class ApiChat(val user: UserRef, val chat: Chat): CR() + @Serializable @SerialName("chatItemInfo") class ApiChatItemInfo(val user: UserRef, val chatItem: AChatItem, val chatItemInfo: ChatItemInfo): CR() + @Serializable @SerialName("userProtoServers") class UserProtoServers(val user: UserRef, val servers: UserProtocolServers): CR() + @Serializable @SerialName("serverTestResult") class ServerTestResult(val user: UserRef, val testServer: String, val testFailure: ProtocolTestFailure? = null): CR() + @Serializable @SerialName("chatItemTTL") class ChatItemTTL(val user: UserRef, val chatItemTTL: Long? = null): CR() @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() - @Serializable @SerialName("contactInfo") class ContactInfo(val user: User, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR() - @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR() - @Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR() - @Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() - @Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR() - @Serializable @SerialName("groupMemberSwitchAborted") class GroupMemberSwitchAborted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() - @Serializable @SerialName("contactSwitch") class ContactSwitch(val user: User, val contact: Contact, val switchProgress: SwitchProgress): CR() - @Serializable @SerialName("groupMemberSwitch") class GroupMemberSwitch(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val switchProgress: SwitchProgress): CR() - @Serializable @SerialName("contactRatchetSyncStarted") class ContactRatchetSyncStarted(val user: User, val contact: Contact, val connectionStats: ConnectionStats): CR() - @Serializable @SerialName("groupMemberRatchetSyncStarted") class GroupMemberRatchetSyncStarted(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() - @Serializable @SerialName("contactRatchetSync") class ContactRatchetSync(val user: User, val contact: Contact, val ratchetSyncProgress: RatchetSyncProgress): CR() - @Serializable @SerialName("groupMemberRatchetSync") class GroupMemberRatchetSync(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val ratchetSyncProgress: RatchetSyncProgress): CR() - @Serializable @SerialName("contactVerificationReset") class ContactVerificationReset(val user: User, val contact: Contact): CR() - @Serializable @SerialName("groupMemberVerificationReset") class GroupMemberVerificationReset(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("contactCode") class ContactCode(val user: User, val contact: Contact, val connectionCode: String): CR() - @Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR() - @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: User, val verified: Boolean, val expectedCode: String): CR() - @Serializable @SerialName("invitation") class Invitation(val user: User, val connReqInvitation: String): CR() - @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: User): CR() - @Serializable @SerialName("sentInvitation") class SentInvitation(val user: User): CR() - @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: User, val contact: Contact): CR() - @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: User, val contact: Contact): CR() - @Serializable @SerialName("chatCleared") class ChatCleared(val user: User, val chatInfo: ChatInfo): CR() + @Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats, val customUserProfile: Profile? = null): CR() + @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR() + @Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("groupMemberSwitchAborted") class GroupMemberSwitchAborted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("contactSwitch") class ContactSwitch(val user: UserRef, val contact: Contact, val switchProgress: SwitchProgress): CR() + @Serializable @SerialName("groupMemberSwitch") class GroupMemberSwitch(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val switchProgress: SwitchProgress): CR() + @Serializable @SerialName("contactRatchetSyncStarted") class ContactRatchetSyncStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("groupMemberRatchetSyncStarted") class GroupMemberRatchetSyncStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() + @Serializable @SerialName("contactRatchetSync") class ContactRatchetSync(val user: UserRef, val contact: Contact, val ratchetSyncProgress: RatchetSyncProgress): CR() + @Serializable @SerialName("groupMemberRatchetSync") class GroupMemberRatchetSync(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val ratchetSyncProgress: RatchetSyncProgress): CR() + @Serializable @SerialName("contactVerificationReset") class ContactVerificationReset(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("groupMemberVerificationReset") class GroupMemberVerificationReset(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("contactCode") class ContactCode(val user: UserRef, val contact: Contact, val connectionCode: String): CR() + @Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR() + @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR() + @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR() + @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() + @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR() + @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR() + @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("chatCleared") class ChatCleared(val user: UserRef, val chatInfo: ChatInfo): CR() @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange(val user: User): CR() - @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile): CR() + @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val user: User, val fromProfile: Profile, val toProfile: Profile, val updateSummary: UserProfileUpdateSummary): CR() @Serializable @SerialName("userPrivacy") class UserPrivacy(val user: User, val updatedUser: User): CR() - @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val user: User, val toContact: Contact): CR() - @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val user: User, val toConnection: PendingContactConnection): CR() - @Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: User, val fromContact: Contact, val toContact: Contact): CR() + @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val user: UserRef, val toContact: Contact): CR() + @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() + @Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: UserRef, val fromContact: Contact, val toContact: Contact): CR() @Serializable @SerialName("userContactLink") class UserContactLink(val user: User, val contactLink: UserContactLinkRec): CR() @Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val user: User, val contactLink: UserContactLinkRec): CR() @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val user: User, val connReqContact: String): CR() @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR() - @Serializable @SerialName("contactConnected") class ContactConnected(val user: User, val contact: Contact, val userCustomProfile: Profile? = null): CR() - @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: User, val contact: Contact): CR() - @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: User, val contactRequest: UserContactRequest): CR() - @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: User, val contact: Contact): CR() - @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: User): CR() - @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: User, val toContact: Contact): CR() + @Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR() + @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest): CR() + @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() + @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR() @Serializable @SerialName("contactsSubscribed") class ContactsSubscribed(val server: String, val contactRefs: List<ContactRef>): CR() @Serializable @SerialName("contactsDisconnected") class ContactsDisconnected(val server: String, val contactRefs: List<ContactRef>): CR() - @Serializable @SerialName("contactSubError") class ContactSubError(val user: User, val contact: Contact, val chatError: ChatError): CR() - @Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: User, val contactSubscriptions: List<ContactSubStatus>): CR() - @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: User, val group: GroupInfo): CR() - @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: User, val memberSubErrors: List<MemberSubError>): CR() - @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: User, val group: GroupInfo): CR() + @Serializable @SerialName("contactSubError") class ContactSubError(val user: UserRef, val contact: Contact, val chatError: ChatError): CR() + @Serializable @SerialName("contactSubSummary") class ContactSubSummary(val user: UserRef, val contactSubscriptions: List<ContactSubStatus>): CR() + @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val user: UserRef, val group: GroupInfo): CR() + @Serializable @SerialName("memberSubErrors") class MemberSubErrors(val user: UserRef, val memberSubErrors: List<MemberSubError>): CR() + @Serializable @SerialName("groupEmpty") class GroupEmpty(val user: UserRef, val group: GroupInfo): CR() @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() - @Serializable @SerialName("newChatItem") class NewChatItem(val user: User, val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: User, val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: User, val chatItem: AChatItem): CR() - @Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: User, val added: Boolean, val reaction: ACIReaction): CR() - @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: User, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() - @Serializable @SerialName("contactsList") class ContactsList(val user: User, val contacts: List<Contact>): CR() + @Serializable @SerialName("newChatItem") class NewChatItem(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemStatusUpdated") class ChatItemStatusUpdated(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR() + @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: UserRef, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() + @Serializable @SerialName("contactsList") class ContactsList(val user: UserRef, val contacts: List<Contact>): CR() // group events - @Serializable @SerialName("groupCreated") class GroupCreated(val user: User, val groupInfo: GroupInfo): CR() - @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: User, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() - @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: User, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() - @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: User, val groupInfo: GroupInfo): CR() - @Serializable @SerialName("groupMembers") class GroupMembers(val user: User, val group: Group): CR() - @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: User, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: User, val groupInfo: GroupInfo): CR() - @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: User, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() - @Serializable @SerialName("memberRole") class MemberRole(val user: User, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("memberRoleUser") class MemberRoleUser(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() - @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("deletedMember") class DeletedMember(val user: User, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() - @Serializable @SerialName("leftMember") class LeftMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("groupDeleted") class GroupDeleted(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("contactsMerged") class ContactsMerged(val user: User, val intoContact: Contact, val mergedContact: Contact): CR() - @Serializable @SerialName("groupInvitation") class GroupInvitation(val user: User, val groupInfo: GroupInfo): CR() // unused - @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: User, val groupInfo: GroupInfo): CR() - @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember): CR() - @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: User, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() - @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: User, val groupInfo: GroupInfo): CR() // unused - @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: User, val toGroup: GroupInfo): CR() - @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupLink") class GroupLink(val user: User, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: User, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() + @Serializable @SerialName("userAcceptedGroupSent") class UserAcceptedGroupSent (val user: UserRef, val groupInfo: GroupInfo, val hostContact: Contact? = null): CR() + @Serializable @SerialName("userDeletedMember") class UserDeletedMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("leftMemberUser") class LeftMemberUser(val user: UserRef, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("groupMembers") class GroupMembers(val user: UserRef, val group: Group): CR() + @Serializable @SerialName("receivedGroupInvitation") class ReceivedGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupDeletedUser") class GroupDeletedUser(val user: UserRef, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("joinedGroupMemberConnecting") class JoinedGroupMemberConnecting(val user: UserRef, val groupInfo: GroupInfo, val hostMember: GroupMember, val member: GroupMember): CR() + @Serializable @SerialName("memberRole") class MemberRole(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("memberRoleUser") class MemberRoleUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val fromRole: GroupMemberRole, val toRole: GroupMemberRole): CR() + @Serializable @SerialName("deletedMemberUser") class DeletedMemberUser(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("deletedMember") class DeletedMember(val user: UserRef, val groupInfo: GroupInfo, val byMember: GroupMember, val deletedMember: GroupMember): CR() + @Serializable @SerialName("leftMember") class LeftMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("groupDeleted") class GroupDeleted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("contactsMerged") class ContactsMerged(val user: UserRef, val intoContact: Contact, val mergedContact: Contact): CR() + @Serializable @SerialName("groupInvitation") class GroupInvitation(val user: UserRef, val groupInfo: GroupInfo): CR() // unused + @Serializable @SerialName("userJoinedGroup") class UserJoinedGroup(val user: UserRef, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() + @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: UserRef, val groupInfo: GroupInfo): CR() // unused + @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() + @Serializable @SerialName("newMemberContactReceivedInv") class NewMemberContactReceivedInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() // receiving file events - @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: User, val chatItem: AChatItem): CR() - @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: User, val rcvFileTransfer: RcvFileTransfer): CR() - @Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: User, val chatItem: AChatItem): CR() - @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: User, val chatItem: AChatItem): CR() - @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() - @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: User, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() - @Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: User, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR() - @Serializable @SerialName("rcvFileError") class RcvFileError(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileStart") class RcvFileStart(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileComplete") class RcvFileComplete(val user: UserRef, val chatItem: AChatItem): CR() + @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileProgressXFTP") class RcvFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val receivedSize: Long, val totalSize: Long): CR() + @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem: AChatItem): CR() // sending file events - @Serializable @SerialName("sndFileStart") class SndFileStart(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR() - @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: User, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() - @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR() - @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: User, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() - @Serializable @SerialName("sndFileError") class SndFileError(val user: User, val chatItem: AChatItem): CR() + @Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndFileCancelled") class SndFileCancelled(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sndFileTransfers: List<SndFileTransfer>): CR() + @Serializable @SerialName("sndFileRcvCancelled") class SndFileRcvCancelled(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() + @Serializable @SerialName("sndFileProgressXFTP") class SndFileProgressXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta, val sentSize: Long, val totalSize: Long): CR() + @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() + @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem: AChatItem): CR() // call events @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() - @Serializable @SerialName("callOffer") class CallOffer(val user: User, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() - @Serializable @SerialName("callAnswer") class CallAnswer(val user: User, val contact: Contact, val answer: WebRTCSession): CR() - @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: User, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() - @Serializable @SerialName("callEnded") class CallEnded(val user: User, val contact: Contact): CR() - @Serializable @SerialName("newContactConnection") class NewContactConnection(val user: User, val connection: PendingContactConnection): CR() - @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: User, val connection: PendingContactConnection): CR() + @Serializable @SerialName("callOffer") class CallOffer(val user: UserRef, val contact: Contact, val callType: CallType, val offer: WebRTCSession, val sharedKey: String? = null, val askConfirmation: Boolean): CR() + @Serializable @SerialName("callAnswer") class CallAnswer(val user: UserRef, val contact: Contact, val answer: WebRTCSession): CR() + @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() + @Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("newContactConnection") class NewContactConnection(val user: UserRef, val connection: PendingContactConnection): CR() + @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("versionInfo") class VersionInfo(val versionInfo: CoreVersionInfo, val chatMigrations: List<UpMigration>, val agentMigrations: List<UpMigration>): CR() - @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List<FormattedText>? = null): CR() - @Serializable @SerialName("cmdOk") class CmdOk(val user: User?): CR() - @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: User?, val chatError: ChatError): CR() - @Serializable @SerialName("chatError") class ChatRespError(val user_: User?, val chatError: ChatError): CR() + @Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR() + @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR() + @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List<ArchiveError>): CR() @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() @@ -3353,9 +3420,11 @@ sealed class CR { is GroupMemberCode -> "groupMemberCode" is ConnectionVerified -> "connectionVerified" is Invitation -> "invitation" + is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" is ContactAlreadyExists -> "contactAlreadyExists" + is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted" is ContactDeleted -> "contactDeleted" is ChatCleared -> "chatCleared" is UserProfileNoChange -> "userProfileNoChange" @@ -3385,6 +3454,7 @@ sealed class CR { is NewChatItem -> "newChatItem" is ChatItemStatusUpdated -> "chatItemStatusUpdated" is ChatItemUpdated -> "chatItemUpdated" + is ChatItemNotChanged -> "chatItemNotChanged" is ChatItemReaction -> "chatItemReaction" is ChatItemDeleted -> "chatItemDeleted" is ContactsList -> "contactsList" @@ -3413,6 +3483,9 @@ sealed class CR { is GroupLinkCreated -> "groupLinkCreated" is GroupLink -> "groupLink" is GroupLinkDeleted -> "groupLinkDeleted" + is NewMemberContact -> "newMemberContact" + is NewMemberContactSentInv -> "newMemberContactSentInv" + is NewMemberContactReceivedInv -> "newMemberContactReceivedInv" is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled" is RcvFileAccepted -> "rcvFileAccepted" is RcvFileStart -> "rcvFileStart" @@ -3436,7 +3509,6 @@ sealed class CR { is NewContactConnection -> "newContactConnection" is ContactConnectionDeleted -> "contactConnectionDeleted" is VersionInfo -> "versionInfo" - is ParsedMarkdown -> "apiParsedMarkdown" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" @@ -3476,9 +3548,11 @@ sealed class CR { is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode") is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") is Invitation -> withUser(user, connReqInvitation) + is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) is SentConfirmation -> withUser(user, noDetails()) is SentInvitation -> withUser(user, noDetails()) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) + is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact)) is ContactDeleted -> withUser(user, json.encodeToString(contact)) is ChatCleared -> withUser(user, json.encodeToString(chatInfo)) is UserProfileNoChange -> withUser(user, noDetails()) @@ -3487,7 +3561,6 @@ sealed class CR { is ContactAliasUpdated -> withUser(user, json.encodeToString(toContact)) is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection)) is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}") - is ParsedMarkdown -> json.encodeToString(formattedText) is UserContactLink -> withUser(user, contactLink.responseDetails) is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails) is UserContactLinkCreated -> withUser(user, connReqContact) @@ -3509,6 +3582,7 @@ sealed class CR { is NewChatItem -> withUser(user, json.encodeToString(chatItem)) is ChatItemStatusUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) + is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}") is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser") is ContactsList -> withUser(user, json.encodeToString(contacts)) @@ -3537,6 +3611,9 @@ sealed class CR { is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) + is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") + is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") + is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is RcvFileAcceptedSndCancelled -> withUser(user, noDetails()) is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) is RcvFileStart -> withUser(user, json.encodeToString(chatItem)) @@ -3572,7 +3649,15 @@ sealed class CR { fun noDetails(): String ="${responseType}: " + generalGetString(MR.strings.no_details) - private fun withUser(u: User?, s: String): String = if (u != null) "userId: ${u.userId}\n$s" else s + private fun withUser(u: UserLike?, s: String): String = if (u != null) "userId: ${u.userId}\n$s" else s +} + +fun chatError(r: CR): ChatErrorType? { + return ( + if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat) r.chatError.errorType + else if (r is CR.ChatRespError && r.chatError is ChatError.ChatErrorChat) r.chatError.errorType + else null + ) } abstract class TerminalItem { @@ -3714,34 +3799,276 @@ sealed class ChatError { @Serializable sealed class ChatErrorType { - val string: String get() = when (this) { - is NoActiveUser -> "noActiveUser" - is DifferentActiveUser -> "differentActiveUser" - is UserExists -> "userExists" - is InvalidConnReq -> "invalidConnReq" - is FileAlreadyReceiving -> "fileAlreadyReceiving" - is СommandError -> "commandError $message" - is CEException -> "exception $message" - } - @Serializable @SerialName("noActiveUser") class NoActiveUser: ChatErrorType() - @Serializable @SerialName("differentActiveUser") class DifferentActiveUser: ChatErrorType() + val string: String + get() = when (this) { + is NoActiveUser -> "noActiveUser" + is NoConnectionUser -> "noConnectionUser" + is NoSndFileUser -> "noSndFileUser" + is NoRcvFileUser -> "noRcvFileUser" + is UserUnknown -> "userUnknown" + is ActiveUserExists -> "activeUserExists" + is UserExists -> "userExists" + is DifferentActiveUser -> "differentActiveUser" + is CantDeleteActiveUser -> "cantDeleteActiveUser" + is CantDeleteLastUser -> "cantDeleteLastUser" + is CantHideLastUser -> "cantHideLastUser" + is HiddenUserAlwaysMuted -> "hiddenUserAlwaysMuted" + is EmptyUserPassword -> "emptyUserPassword" + is UserAlreadyHidden -> "userAlreadyHidden" + is UserNotHidden -> "userNotHidden" + is ChatNotStarted -> "chatNotStarted" + is ChatNotStopped -> "chatNotStopped" + is ChatStoreChanged -> "chatStoreChanged" + is InvalidConnReq -> "invalidConnReq" + is InvalidChatMessage -> "invalidChatMessage" + is ContactNotReady -> "contactNotReady" + is ContactDisabled -> "contactDisabled" + is ConnectionDisabled -> "connectionDisabled" + is GroupUserRole -> "groupUserRole" + is GroupMemberInitialRole -> "groupMemberInitialRole" + is ContactIncognitoCantInvite -> "contactIncognitoCantInvite" + is GroupIncognitoCantInvite -> "groupIncognitoCantInvite" + is GroupContactRole -> "groupContactRole" + is GroupDuplicateMember -> "groupDuplicateMember" + is GroupDuplicateMemberId -> "groupDuplicateMemberId" + is GroupNotJoined -> "groupNotJoined" + is GroupMemberNotActive -> "groupMemberNotActive" + is GroupMemberUserRemoved -> "groupMemberUserRemoved" + is GroupMemberNotFound -> "groupMemberNotFound" + is GroupMemberIntroNotFound -> "groupMemberIntroNotFound" + is GroupCantResendInvitation -> "groupCantResendInvitation" + is GroupInternal -> "groupInternal" + is FileNotFound -> "fileNotFound" + is FileSize -> "fileSize" + is FileAlreadyReceiving -> "fileAlreadyReceiving" + is FileCancelled -> "fileCancelled" + is FileCancel -> "fileCancel" + is FileAlreadyExists -> "fileAlreadyExists" + is FileRead -> "fileRead" + is FileWrite -> "fileWrite" + is FileSend -> "fileSend" + is FileRcvChunk -> "fileRcvChunk" + is FileInternal -> "fileInternal" + is FileImageType -> "fileImageType" + is FileImageSize -> "fileImageSize" + is FileNotReceived -> "fileNotReceived" + // is XFTPRcvFile -> "xftpRcvFile" + // is XFTPSndFile -> "xftpSndFile" + is FallbackToSMPProhibited -> "fallbackToSMPProhibited" + is InlineFileProhibited -> "inlineFileProhibited" + is InvalidQuote -> "invalidQuote" + is InvalidChatItemUpdate -> "invalidChatItemUpdate" + is InvalidChatItemDelete -> "invalidChatItemDelete" + is HasCurrentCall -> "hasCurrentCall" + is NoCurrentCall -> "noCurrentCall" + is CallContact -> "callContact" + is CallState -> "callState" + is DirectMessagesProhibited -> "directMessagesProhibited" + is AgentVersion -> "agentVersion" + is AgentNoSubResult -> "agentNoSubResult" + is CommandError -> "commandError $message" + is ServerProtocol -> "serverProtocol" + is AgentCommandError -> "agentCommandError" + is InvalidFileDescription -> "invalidFileDescription" + is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited" + is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible" + is InternalError -> "internalError" + is CEException -> "exception $message" + } + + @Serializable @SerialName("noActiveUser") object NoActiveUser: ChatErrorType() + @Serializable @SerialName("noConnectionUser") class NoConnectionUser(val agentConnId: String): ChatErrorType() + @Serializable @SerialName("noSndFileUser") class NoSndFileUser(val agentSndFileId: String): ChatErrorType() + @Serializable @SerialName("noRcvFileUser") class NoRcvFileUser(val agentRcvFileId: String): ChatErrorType() + @Serializable @SerialName("userUnknown") object UserUnknown: ChatErrorType() + @Serializable @SerialName("activeUserExists") object ActiveUserExists: ChatErrorType() @Serializable @SerialName("userExists") class UserExists(val contactName: String): ChatErrorType() - @Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType() - @Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving: ChatErrorType() - @Serializable @SerialName("commandError") class СommandError(val message: String): ChatErrorType() + @Serializable @SerialName("differentActiveUser") class DifferentActiveUser(val commandUserId: Long, val activeUserId: Long): ChatErrorType() + @Serializable @SerialName("cantDeleteActiveUser") class CantDeleteActiveUser(val userId: Long): ChatErrorType() + @Serializable @SerialName("cantDeleteLastUser") class CantDeleteLastUser(val userId: Long): ChatErrorType() + @Serializable @SerialName("cantHideLastUser") class CantHideLastUser(val userId: Long): ChatErrorType() + @Serializable @SerialName("hiddenUserAlwaysMuted") class HiddenUserAlwaysMuted(val userId: Long): ChatErrorType() + @Serializable @SerialName("emptyUserPassword") class EmptyUserPassword(val userId: Long): ChatErrorType() + @Serializable @SerialName("userAlreadyHidden") class UserAlreadyHidden(val userId: Long): ChatErrorType() + @Serializable @SerialName("userNotHidden") class UserNotHidden(val userId: Long): ChatErrorType() + @Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType() + @Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType() + @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() + @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() + @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() + @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() + @Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType() + @Serializable @SerialName("connectionDisabled") class ConnectionDisabled(val connection: Connection): ChatErrorType() + @Serializable @SerialName("groupUserRole") class GroupUserRole(val groupInfo: GroupInfo, val requiredRole: GroupMemberRole): ChatErrorType() + @Serializable @SerialName("groupMemberInitialRole") class GroupMemberInitialRole(val groupInfo: GroupInfo, val initialRole: GroupMemberRole): ChatErrorType() + @Serializable @SerialName("contactIncognitoCantInvite") object ContactIncognitoCantInvite: ChatErrorType() + @Serializable @SerialName("groupIncognitoCantInvite") object GroupIncognitoCantInvite: ChatErrorType() + @Serializable @SerialName("groupContactRole") class GroupContactRole(val contactName: String): ChatErrorType() + @Serializable @SerialName("groupDuplicateMember") class GroupDuplicateMember(val contactName: String): ChatErrorType() + @Serializable @SerialName("groupDuplicateMemberId") object GroupDuplicateMemberId: ChatErrorType() + @Serializable @SerialName("groupNotJoined") class GroupNotJoined(val groupInfo: GroupInfo): ChatErrorType() + @Serializable @SerialName("groupMemberNotActive") object GroupMemberNotActive: ChatErrorType() + @Serializable @SerialName("groupMemberUserRemoved") object GroupMemberUserRemoved: ChatErrorType() + @Serializable @SerialName("groupMemberNotFound") object GroupMemberNotFound: ChatErrorType() + @Serializable @SerialName("groupMemberIntroNotFound") class GroupMemberIntroNotFound(val contactName: String): ChatErrorType() + @Serializable @SerialName("groupCantResendInvitation") class GroupCantResendInvitation(val groupInfo: GroupInfo, val contactName: String): ChatErrorType() + @Serializable @SerialName("groupInternal") class GroupInternal(val message: String): ChatErrorType() + @Serializable @SerialName("fileNotFound") class FileNotFound(val message: String): ChatErrorType() + @Serializable @SerialName("fileSize") class FileSize(val filePath: String): ChatErrorType() + @Serializable @SerialName("fileAlreadyReceiving") class FileAlreadyReceiving(val message: String): ChatErrorType() + @Serializable @SerialName("fileCancelled") class FileCancelled(val message: String): ChatErrorType() + @Serializable @SerialName("fileCancel") class FileCancel(val fileId: Long, val message: String): ChatErrorType() + @Serializable @SerialName("fileAlreadyExists") class FileAlreadyExists(val filePath: String): ChatErrorType() + @Serializable @SerialName("fileRead") class FileRead(val filePath: String, val message: String): ChatErrorType() + @Serializable @SerialName("fileWrite") class FileWrite(val filePath: String, val message: String): ChatErrorType() + @Serializable @SerialName("fileSend") class FileSend(val fileId: Long, val agentError: String): ChatErrorType() + @Serializable @SerialName("fileRcvChunk") class FileRcvChunk(val message: String): ChatErrorType() + @Serializable @SerialName("fileInternal") class FileInternal(val message: String): ChatErrorType() + @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() + @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() + @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() + // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() + // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() + @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() + @Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType() + @Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType() + @Serializable @SerialName("invalidChatItemUpdate") object InvalidChatItemUpdate: ChatErrorType() + @Serializable @SerialName("invalidChatItemDelete") object InvalidChatItemDelete: ChatErrorType() + @Serializable @SerialName("hasCurrentCall") object HasCurrentCall: ChatErrorType() + @Serializable @SerialName("noCurrentCall") object NoCurrentCall: ChatErrorType() + @Serializable @SerialName("callContact") class CallContact(val contactId: Long): ChatErrorType() + @Serializable @SerialName("callState") object CallState: ChatErrorType() + @Serializable @SerialName("directMessagesProhibited") class DirectMessagesProhibited(val contact: Contact): ChatErrorType() + @Serializable @SerialName("agentVersion") object AgentVersion: ChatErrorType() + @Serializable @SerialName("agentNoSubResult") class AgentNoSubResult(val agentConnId: String): ChatErrorType() + @Serializable @SerialName("commandError") class CommandError(val message: String): ChatErrorType() + @Serializable @SerialName("serverProtocol") object ServerProtocol: ChatErrorType() + @Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType() + @Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType() + @Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType() + @Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType() + @Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType() @Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType() } @Serializable sealed class StoreError { - val string: String get() = when (this) { - is UserContactLinkNotFound -> "userContactLinkNotFound" - is GroupNotFound -> "groupNotFound" - is DuplicateName -> "duplicateName" - } - @Serializable @SerialName("userContactLinkNotFound") class UserContactLinkNotFound: StoreError() - @Serializable @SerialName("groupNotFound") class GroupNotFound: StoreError() - @Serializable @SerialName("duplicateName") class DuplicateName: StoreError() + val string: String + get() = when (this) { + is DuplicateName -> "duplicateName" + is UserNotFound -> "userNotFound" + is UserNotFoundByName -> "userNotFoundByName" + is UserNotFoundByContactId -> "userNotFoundByContactId" + is UserNotFoundByGroupId -> "userNotFoundByGroupId" + is UserNotFoundByFileId -> "userNotFoundByFileId" + is UserNotFoundByContactRequestId -> "userNotFoundByContactRequestId" + is ContactNotFound -> "contactNotFound" + is ContactNotFoundByName -> "contactNotFoundByName" + is ContactNotFoundByMemberId -> "contactNotFoundByMemberId" + is ContactNotReady -> "contactNotReady" + is DuplicateContactLink -> "duplicateContactLink" + is UserContactLinkNotFound -> "userContactLinkNotFound" + is ContactRequestNotFound -> "contactRequestNotFound" + is ContactRequestNotFoundByName -> "contactRequestNotFoundByName" + is GroupNotFound -> "groupNotFound" + is GroupNotFoundByName -> "groupNotFoundByName" + is GroupMemberNameNotFound -> "groupMemberNameNotFound" + is GroupMemberNotFound -> "groupMemberNotFound" + is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId" + is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound" + is GroupWithoutUser -> "groupWithoutUser" + is DuplicateGroupMember -> "duplicateGroupMember" + is GroupAlreadyJoined -> "groupAlreadyJoined" + is GroupInvitationNotFound -> "groupInvitationNotFound" + is SndFileNotFound -> "sndFileNotFound" + is SndFileInvalid -> "sndFileInvalid" + is RcvFileNotFound -> "rcvFileNotFound" + is RcvFileDescrNotFound -> "rcvFileDescrNotFound" + is FileNotFound -> "fileNotFound" + is RcvFileInvalid -> "rcvFileInvalid" + is RcvFileInvalidDescrPart -> "rcvFileInvalidDescrPart" + is SharedMsgIdNotFoundByFileId -> "sharedMsgIdNotFoundByFileId" + is FileIdNotFoundBySharedMsgId -> "fileIdNotFoundBySharedMsgId" + is SndFileNotFoundXFTP -> "sndFileNotFoundXFTP" + is RcvFileNotFoundXFTP -> "rcvFileNotFoundXFTP" + is ConnectionNotFound -> "connectionNotFound" + is ConnectionNotFoundById -> "connectionNotFoundById" + is ConnectionNotFoundByMemberId -> "connectionNotFoundByMemberId" + is PendingConnectionNotFound -> "pendingConnectionNotFound" + is IntroNotFound -> "introNotFound" + is UniqueID -> "uniqueID" + is InternalError -> "internalError" + is NoMsgDelivery -> "noMsgDelivery" + is BadChatItem -> "badChatItem" + is ChatItemNotFound -> "chatItemNotFound" + is ChatItemNotFoundByText -> "chatItemNotFoundByText" + is ChatItemSharedMsgIdNotFound -> "chatItemSharedMsgIdNotFound" + is ChatItemNotFoundByFileId -> "chatItemNotFoundByFileId" + is ChatItemNotFoundByGroupId -> "chatItemNotFoundByGroupId" + is ProfileNotFound -> "profileNotFound" + is DuplicateGroupLink -> "duplicateGroupLink" + is GroupLinkNotFound -> "groupLinkNotFound" + is HostMemberIdNotFound -> "hostMemberIdNotFound" + is ContactNotFoundByFileId -> "contactNotFoundByFileId" + is NoGroupSndStatus -> "noGroupSndStatus" + } + + @Serializable @SerialName("duplicateName") object DuplicateName: StoreError() + @Serializable @SerialName("userNotFound") class UserNotFound(val userId: Long): StoreError() + @Serializable @SerialName("userNotFoundByName") class UserNotFoundByName(val contactName: String): StoreError() + @Serializable @SerialName("userNotFoundByContactId") class UserNotFoundByContactId(val contactId: Long): StoreError() + @Serializable @SerialName("userNotFoundByGroupId") class UserNotFoundByGroupId(val groupId: Long): StoreError() + @Serializable @SerialName("userNotFoundByFileId") class UserNotFoundByFileId(val fileId: Long): StoreError() + @Serializable @SerialName("userNotFoundByContactRequestId") class UserNotFoundByContactRequestId(val contactRequestId: Long): StoreError() + @Serializable @SerialName("contactNotFound") class ContactNotFound(val contactId: Long): StoreError() + @Serializable @SerialName("contactNotFoundByName") class ContactNotFoundByName(val contactName: String): StoreError() + @Serializable @SerialName("contactNotFoundByMemberId") class ContactNotFoundByMemberId(val groupMemberId: Long): StoreError() + @Serializable @SerialName("contactNotReady") class ContactNotReady(val contactName: String): StoreError() + @Serializable @SerialName("duplicateContactLink") object DuplicateContactLink: StoreError() + @Serializable @SerialName("userContactLinkNotFound") object UserContactLinkNotFound: StoreError() + @Serializable @SerialName("contactRequestNotFound") class ContactRequestNotFound(val contactRequestId: Long): StoreError() + @Serializable @SerialName("contactRequestNotFoundByName") class ContactRequestNotFoundByName(val contactName: String): StoreError() + @Serializable @SerialName("groupNotFound") class GroupNotFound(val groupId: Long): StoreError() + @Serializable @SerialName("groupNotFoundByName") class GroupNotFoundByName(val groupName: String): StoreError() + @Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError() + @Serializable @SerialName("groupMemberNotFound") class GroupMemberNotFound(val groupMemberId: Long): StoreError() + @Serializable @SerialName("groupMemberNotFoundByMemberId") class GroupMemberNotFoundByMemberId(val memberId: String): StoreError() + @Serializable @SerialName("memberContactGroupMemberNotFound") class MemberContactGroupMemberNotFound(val contactId: Long): StoreError() + @Serializable @SerialName("groupWithoutUser") object GroupWithoutUser: StoreError() + @Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError() + @Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError() + @Serializable @SerialName("groupInvitationNotFound") object GroupInvitationNotFound: StoreError() + @Serializable @SerialName("sndFileNotFound") class SndFileNotFound(val fileId: Long): StoreError() + @Serializable @SerialName("sndFileInvalid") class SndFileInvalid(val fileId: Long): StoreError() + @Serializable @SerialName("rcvFileNotFound") class RcvFileNotFound(val fileId: Long): StoreError() + @Serializable @SerialName("rcvFileDescrNotFound") class RcvFileDescrNotFound(val fileId: Long): StoreError() + @Serializable @SerialName("fileNotFound") class FileNotFound(val fileId: Long): StoreError() + @Serializable @SerialName("rcvFileInvalid") class RcvFileInvalid(val fileId: Long): StoreError() + @Serializable @SerialName("rcvFileInvalidDescrPart") object RcvFileInvalidDescrPart: StoreError() + @Serializable @SerialName("sharedMsgIdNotFoundByFileId") class SharedMsgIdNotFoundByFileId(val fileId: Long): StoreError() + @Serializable @SerialName("fileIdNotFoundBySharedMsgId") class FileIdNotFoundBySharedMsgId(val sharedMsgId: String): StoreError() + @Serializable @SerialName("sndFileNotFoundXFTP") class SndFileNotFoundXFTP(val agentSndFileId: String): StoreError() + @Serializable @SerialName("rcvFileNotFoundXFTP") class RcvFileNotFoundXFTP(val agentRcvFileId: String): StoreError() + @Serializable @SerialName("connectionNotFound") class ConnectionNotFound(val agentConnId: String): StoreError() + @Serializable @SerialName("connectionNotFoundById") class ConnectionNotFoundById(val connId: Long): StoreError() + @Serializable @SerialName("connectionNotFoundByMemberId") class ConnectionNotFoundByMemberId(val groupMemberId: Long): StoreError() + @Serializable @SerialName("pendingConnectionNotFound") class PendingConnectionNotFound(val connId: Long): StoreError() + @Serializable @SerialName("introNotFound") object IntroNotFound: StoreError() + @Serializable @SerialName("uniqueID") object UniqueID: StoreError() + @Serializable @SerialName("internalError") class InternalError(val message: String): StoreError() + @Serializable @SerialName("noMsgDelivery") class NoMsgDelivery(val connId: Long, val agentMsgId: String): StoreError() + @Serializable @SerialName("badChatItem") class BadChatItem(val itemId: Long): StoreError() + @Serializable @SerialName("chatItemNotFound") class ChatItemNotFound(val itemId: Long): StoreError() + @Serializable @SerialName("chatItemNotFoundByText") class ChatItemNotFoundByText(val text: String): StoreError() + @Serializable @SerialName("chatItemSharedMsgIdNotFound") class ChatItemSharedMsgIdNotFound(val sharedMsgId: String): StoreError() + @Serializable @SerialName("chatItemNotFoundByFileId") class ChatItemNotFoundByFileId(val fileId: Long): StoreError() + @Serializable @SerialName("chatItemNotFoundByGroupId") class ChatItemNotFoundByGroupId(val groupId: Long): StoreError() + @Serializable @SerialName("profileNotFound") class ProfileNotFound(val profileId: Long): StoreError() + @Serializable @SerialName("duplicateGroupLink") class DuplicateGroupLink(val groupInfo: GroupInfo): StoreError() + @Serializable @SerialName("groupLinkNotFound") class GroupLinkNotFound(val groupInfo: GroupInfo): StoreError() + @Serializable @SerialName("hostMemberIdNotFound") class HostMemberIdNotFound(val groupId: Long): StoreError() + @Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError() + @Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError() } @Serializable @@ -3772,18 +4099,22 @@ sealed class AgentErrorType { is CMD -> "CMD ${cmdErr.string}" is CONN -> "CONN ${connErr.string}" is SMP -> "SMP ${smpErr.string}" + // is NTF -> "NTF ${ntfErr.string}" is XFTP -> "XFTP ${xftpErr.string}" is BROKER -> "BROKER ${brokerErr.string}" is AGENT -> "AGENT ${agentErr.string}" is INTERNAL -> "INTERNAL $internalErr" + is INACTIVE -> "INACTIVE" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType() + // @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType() @Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType() @Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() + @Serializable @SerialName("INACTIVE") object INACTIVE: AgentErrorType() } @Serializable @@ -3821,17 +4152,19 @@ sealed class ConnectionErrorType { @Serializable sealed class BrokerErrorType { val string: String get() = when (this) { - is RESPONSE -> "RESPONSE ${smpErr.string}" + is RESPONSE -> "RESPONSE ${smpErr}" is UNEXPECTED -> "UNEXPECTED" is NETWORK -> "NETWORK" + is HOST -> "HOST" is TRANSPORT -> "TRANSPORT ${transportErr.string}" is TIMEOUT -> "TIMEOUT" } - @Serializable @SerialName("RESPONSE") class RESPONSE(val smpErr: SMPErrorType): BrokerErrorType() - @Serializable @SerialName("UNEXPECTED") class UNEXPECTED: BrokerErrorType() - @Serializable @SerialName("NETWORK") class NETWORK: BrokerErrorType() + @Serializable @SerialName("RESPONSE") class RESPONSE(val smpErr: String): BrokerErrorType() + @Serializable @SerialName("UNEXPECTED") object UNEXPECTED: BrokerErrorType() + @Serializable @SerialName("NETWORK") object NETWORK: BrokerErrorType() + @Serializable @SerialName("HOST") object HOST: BrokerErrorType() @Serializable @SerialName("TRANSPORT") class TRANSPORT(val transportErr: SMPTransportError): BrokerErrorType() - @Serializable @SerialName("TIMEOUT") class TIMEOUT: BrokerErrorType() + @Serializable @SerialName("TIMEOUT") object TIMEOUT: BrokerErrorType() } @Serializable @@ -3861,15 +4194,17 @@ sealed class ProtocolCommandError { val string: String get() = when (this) { is UNKNOWN -> "UNKNOWN" is SYNTAX -> "SYNTAX" + is PROHIBITED -> "PROHIBITED" is NO_AUTH -> "NO_AUTH" is HAS_AUTH -> "HAS_AUTH" is NO_QUEUE -> "NO_QUEUE" } - @Serializable @SerialName("UNKNOWN") class UNKNOWN: ProtocolCommandError() - @Serializable @SerialName("SYNTAX") class SYNTAX: ProtocolCommandError() - @Serializable @SerialName("NO_AUTH") class NO_AUTH: ProtocolCommandError() - @Serializable @SerialName("HAS_AUTH") class HAS_AUTH: ProtocolCommandError() - @Serializable @SerialName("NO_QUEUE") class NO_QUEUE: ProtocolCommandError() + @Serializable @SerialName("UNKNOWN") object UNKNOWN: ProtocolCommandError() + @Serializable @SerialName("SYNTAX") object SYNTAX: ProtocolCommandError() + @Serializable @SerialName("PROHIBITED") object PROHIBITED: ProtocolCommandError() + @Serializable @SerialName("NO_AUTH") object NO_AUTH: ProtocolCommandError() + @Serializable @SerialName("HAS_AUTH") object HAS_AUTH: ProtocolCommandError() + @Serializable @SerialName("NO_QUEUE") object NO_QUEUE: ProtocolCommandError() } @Serializable @@ -3904,12 +4239,16 @@ sealed class SMPAgentError { is A_MESSAGE -> "A_MESSAGE" is A_PROHIBITED -> "A_PROHIBITED" is A_VERSION -> "A_VERSION" - is A_ENCRYPTION -> "A_ENCRYPTION" + is A_CRYPTO -> "A_CRYPTO" + is A_DUPLICATE -> "A_DUPLICATE" + is A_QUEUE -> "A_QUEUE" } - @Serializable @SerialName("A_MESSAGE") class A_MESSAGE: SMPAgentError() - @Serializable @SerialName("A_PROHIBITED") class A_PROHIBITED: SMPAgentError() - @Serializable @SerialName("A_VERSION") class A_VERSION: SMPAgentError() - @Serializable @SerialName("A_ENCRYPTION") class A_ENCRYPTION: SMPAgentError() + @Serializable @SerialName("A_MESSAGE") object A_MESSAGE: SMPAgentError() + @Serializable @SerialName("A_PROHIBITED") object A_PROHIBITED: SMPAgentError() + @Serializable @SerialName("A_VERSION") object A_VERSION: SMPAgentError() + @Serializable @SerialName("A_CRYPTO") object A_CRYPTO: SMPAgentError() + @Serializable @SerialName("A_DUPLICATE") object A_DUPLICATE: SMPAgentError() + @Serializable @SerialName("A_QUEUE") class A_QUEUE(val queueErr: String): SMPAgentError() } @Serializable @@ -3951,3 +4290,12 @@ sealed class ArchiveError { @Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError() @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() } + + +enum class NotificationsMode() { + OFF, PERIODIC, SERVICE, /*INSTANT - for Firebase notifications */; + + companion object { + val default: NotificationsMode = SERVICE + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt index ed2533b488..9760e9c9f2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt @@ -2,7 +2,7 @@ package com.sd.lib.compose.wheel_picker import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme +import chat.simplex.common.ui.theme.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt new file mode 100644 index 0000000000..d36a6aec16 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -0,0 +1,49 @@ +package chat.simplex.common.platform + +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.DefaultTheme +import java.io.File +import java.util.* + +enum class AppPlatform { + ANDROID, DESKTOP; + + val isAndroid: Boolean + get() = this == ANDROID + + val isDesktop: Boolean + get() = this == DESKTOP +} + +expect val appPlatform: AppPlatform + +val appVersionInfo: Pair<String, Int?> = if (appPlatform == AppPlatform.ANDROID) + BuildConfigCommon.ANDROID_VERSION_NAME to BuildConfigCommon.ANDROID_VERSION_CODE +else + BuildConfigCommon.DESKTOP_VERSION_NAME to null + +class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() { + override fun add(element: E): Boolean { + if(size > capacity) removeFirst() + return super.add(element) + } +} + +// LALAL VERSION CODE +fun runMigrations() { + val lastMigration = ChatController.appPrefs.lastMigratedVersionCode + if (lastMigration.get() < BuildConfigCommon.ANDROID_VERSION_CODE) { + while (true) { + if (lastMigration.get() < 117) { + if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.DARK.name) { + ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) + } + lastMigration.set(117) + } else { + lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE) + break + } + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Back.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Back.kt new file mode 100644 index 0000000000..6096cc3952 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Back.kt @@ -0,0 +1,7 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* + +@SuppressWarnings("MissingJvmstatic") +@Composable +expect fun BackHandler(enabled: Boolean = true, onBack: () -> Unit) 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 new file mode 100644 index 0000000000..801a0270e2 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -0,0 +1,74 @@ +package chat.simplex.common.platform + +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import kotlinx.serialization.decodeFromString +import java.nio.ByteBuffer + +// ghc's rts +external fun initHS() +// android-support +external fun pipeStdOutToSocket(socketName: String) : Int + +// SimpleX API +typealias ChatCtrl = Long +external fun chatMigrateInit(dbPath: String, dbKey: String, confirm: String): Array<Any> +external fun chatSendCmd(ctrl: ChatCtrl, msg: String): String +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 chatPasswordHash(pwd: String, salt: String): String +external fun chatWriteFile(path: String, buffer: ByteBuffer): String +external fun chatReadFile(path: String, key: String, nonce: String): Array<Any> +external fun chatEncryptFile(fromPath: String, toPath: String): String +external fun chatDecryptFile(fromPath: String, key: String, nonce: String, toPath: String): String + +val chatModel: ChatModel + get() = chatController.chatModel + +val appPreferences: AppPreferences + get() = chatController.appPrefs + +val chatController: ChatController = ChatController + +suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: Boolean = true) { + val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() + val confirm = confirmMigrations ?: if (appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp + val migrated: Array<Any> = chatMigrateInit(dbAbsolutePrefixPath, dbKey, confirm.value) + val res: DBMigrationResult = kotlin.runCatching { + json.decodeFromString<DBMigrationResult>(migrated[0] as String) + }.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) } + val ctrl = if (res is DBMigrationResult.OK) { + migrated[1] as Long + } else null + chatController.ctrl = ctrl + chatModel.chatDbEncrypted.value = dbKey != "" + chatModel.chatDbStatus.value = res + if (res != DBMigrationResult.OK) { + Log.d(TAG, "Unable to migrate successfully: $res") + } else if (startChat) { + // If we migrated successfully means previous re-encryption process on database level finished successfully too + if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) + val user = chatController.apiGetActiveUser() + if (user == null) { + chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) + chatModel.currentUser.value = null + chatModel.users.clear() + } else { + val savedOnboardingStage = appPreferences.onboardingStage.get() + appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) { + OnboardingStage.Step3_CreateSimpleXAddress + } else { + savedOnboardingStage + }) + if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) { + chatModel.setDeliveryReceipts.value = true + } + chatController.startChat(user) + platform.androidChatInitializedAndStarted() + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt new file mode 100644 index 0000000000..5af941fb42 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.platform + +interface CryptorInterface { + fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? + fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> + fun deleteKey(alias: String) +} + +expect val cryptor: CryptorInterface diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt new file mode 100644 index 0000000000..71a9f204f8 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -0,0 +1,102 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.CIFile +import chat.simplex.common.model.CryptoFile +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import java.io.* +import java.net.URI + +expect val dataDir: File +expect val tmpDir: File +expect val filesDir: File +expect val appFilesDir: File +expect val coreTmpDir: File +expect val dbAbsolutePrefixPath: String + +expect val chatDatabaseFileName: String +expect val agentDatabaseFileName: String + +/** +* This is used only for temporary storing db archive for export. +* Providing [tmpDir] instead crashes the app. Check db export before moving from this path to something else +* */ +expect val databaseExportDir: File + +expect fun desktopOpenDatabaseDir() + +fun copyFileToFile(from: File, to: URI, finally: () -> Unit) { + try { + to.outputStream().use { stream -> + BufferedOutputStream(stream).use { outputStream -> + from.inputStream().use { it.copyTo(outputStream) } + } + } + showToast(generalGetString(MR.strings.file_saved)) + } catch (e: Error) { + showToast(generalGetString(MR.strings.error_saving_file)) + Log.e(TAG, "copyFileToFile error saving file $e") + } finally { + finally() + } +} + +fun copyBytesToFile(bytes: ByteArrayInputStream, to: URI, finally: () -> Unit) { + try { + to.outputStream().use { stream -> + BufferedOutputStream(stream).use { outputStream -> + bytes.use { it.copyTo(outputStream) } + } + } + showToast(generalGetString(MR.strings.file_saved)) + } catch (e: Error) { + showToast(generalGetString(MR.strings.error_saving_file)) + Log.e(TAG, "copyBytesToFile error saving file $e") + } finally { + finally() + } +} + +fun getAppFilePath(fileName: String): String { + return appFilesDir.absolutePath + File.separator + fileName +} + +fun getLoadedFilePath(file: CIFile?): String? { + val f = file?.fileSource?.filePath + return if (f != null && file.loaded) { + val filePath = getAppFilePath(f) + if (File(filePath).exists()) filePath else null + } else { + null + } +} + +fun getLoadedFileSource(file: CIFile?): CryptoFile? { + val f = file?.fileSource?.filePath + return if (f != null && file.loaded) { + val filePath = getAppFilePath(f) + if (File(filePath).exists()) file.fileSource else null + } else { + null + } +} + +/** +* [rememberedValue] is used in `remember(rememberedValue)`. So when the value changes, file saver will update a callback function +* */ +@Composable +expect fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any? = null, onResult: (URI?) -> Unit): FileChooserLauncher + +expect fun rememberFileChooserMultipleLauncher(onResult: (List<URI>) -> Unit): FileChooserMultipleLauncher + +expect class FileChooserLauncher() { + suspend fun launch(input: String) +} + +expect class FileChooserMultipleLauncher() { + suspend fun launch(input: String) +} + +expect fun URI.inputStream(): InputStream? +expect fun URI.outputStream(): OutputStream diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt new file mode 100644 index 0000000000..fca69d5398 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Images.kt @@ -0,0 +1,25 @@ +package chat.simplex.common.platform + +import androidx.compose.ui.graphics.ImageBitmap +import boofcv.struct.image.GrayU8 +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.net.URI + +expect fun base64ToBitmap(base64ImageString: String): ImageBitmap +expect fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String +expect fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream +expect fun cropToSquare(image: ImageBitmap): ImageBitmap +expect fun compressImageStr(bitmap: ImageBitmap): String +expect fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream + +expect fun GrayU8.toImageBitmap(): ImageBitmap + +expect fun ImageBitmap.hasAlpha(): Boolean +expect fun ImageBitmap.addLogo(): ImageBitmap +expect fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap + +expect fun isImage(uri: URI): Boolean +expect fun isAnimImage(uri: URI, drawable: Any?): Boolean + +expect fun loadImageBitmap(inputStream: InputStream): ImageBitmap diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt new file mode 100644 index 0000000000..a1b39527d1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Log.kt @@ -0,0 +1,10 @@ +package chat.simplex.common.platform + +const val TAG = "SIMPLEX" + +expect object Log { + fun d(tag: String, text: String) + fun e(tag: String, text: String) + fun i(tag: String, text: String) + fun w(tag: String, text: String) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt new file mode 100644 index 0000000000..543444d0eb --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -0,0 +1,22 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter + +expect fun Modifier.navigationBarsWithImePadding(): Modifier + +@Composable +expect fun ProvideWindowInsets( + consumeWindowInsets: Boolean = true, + windowInsetsAnimationsEnabled: Boolean = true, + content: @Composable () -> Unit +) + +@Composable +expect fun Modifier.desktopOnExternalDrag( + enabled: Boolean = true, + onFiles: (List<String>) -> Unit = {}, + onImage: (Painter) -> Unit = {}, + onText: (String) -> Unit = {} +): Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt new file mode 100644 index 0000000000..14d6f1701e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Notifications.kt @@ -0,0 +1,3 @@ +package chat.simplex.common.platform + +expect fun allowedToShowNotification(): Boolean diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt new file mode 100644 index 0000000000..a03df5addb --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -0,0 +1,123 @@ +package chat.simplex.common.platform + +import chat.simplex.common.model.* +import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.views.chatlist.acceptContactRequest +import chat.simplex.common.views.chatlist.openChat +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +enum class NotificationAction { + ACCEPT_CONTACT_REQUEST, + ACCEPT_CONTACT_REQUEST_INCOGNITO +} + +lateinit var ntfManager: NtfManager + +abstract class NtfManager { + fun notifyContactConnected(user: UserLike, contact: Contact) = displayNotification( + user = user, + chatId = contact.id, + displayName = contact.displayName, + msgText = generalGetString(MR.strings.notification_contact_connected) + ) + + fun notifyContactRequestReceived(user: UserLike, cInfo: ChatInfo.ContactRequest) = displayNotification( + user = user, + chatId = cInfo.id, + displayName = cInfo.displayName, + msgText = generalGetString(MR.strings.notification_new_contact_request), + image = cInfo.image, + listOf( + NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) }, + NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO to { acceptContactRequestAction(user.userId, incognito = true, cInfo.id) } + ) + ) + + fun notifyMessageReceived(user: UserLike, cInfo: ChatInfo, cItem: ChatItem) { + if (!cInfo.ntfsEnabled) return + displayNotification(user = user, chatId = cInfo.id, displayName = cInfo.displayName, msgText = hideSecrets(cItem)) + } + + fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) { + val isCurrentUser = ChatModel.currentUser.value?.userId == userId + val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) { + (ChatModel.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return + } else { + null + } + val apiId = chatId.replace("<@", "").toLongOrNull() ?: return + acceptContactRequest(incognito, apiId, cInfo, isCurrentUser, ChatModel) + cancelNotificationsForChat(chatId) + } + + fun openChatAction(userId: Long?, chatId: ChatId) { + withBGApi { + awaitChatStartedIfNeeded(chatModel) + if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { + chatModel.controller.changeActiveUser(userId, null) + } + val cInfo = chatModel.getChat(chatId)?.chatInfo + chatModel.clearOverlays.value = true + if (cInfo != null && (cInfo is ChatInfo.Direct || cInfo is ChatInfo.Group)) openChat(cInfo, chatModel) + } + } + + fun showChatsAction(userId: Long?) { + withBGApi { + awaitChatStartedIfNeeded(chatModel) + if (userId != null && userId != chatModel.currentUser.value?.userId && chatModel.currentUser.value != null) { + chatModel.controller.changeActiveUser(userId, null) + } + chatModel.chatId.value = null + chatModel.clearOverlays.value = true + } + } + + fun acceptCallAction(chatId: ChatId) { + chatModel.clearOverlays.value = true + val invitation = chatModel.callInvitations[chatId] + if (invitation == null) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) + } else { + chatModel.callManager.acceptIncomingCall(invitation = invitation) + } + } + + abstract fun notifyCallInvitation(invitation: RcvCallInvitation) + abstract fun hasNotificationsForChat(chatId: String): Boolean + abstract fun cancelNotificationsForChat(chatId: String) + abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List<Pair<NotificationAction, () -> Unit>> = emptyList()) + abstract fun cancelCallNotification() + abstract fun cancelAllNotifications() + // Android only + abstract fun androidCreateNtfChannelsMaybeShowAlert() + + private suspend fun awaitChatStartedIfNeeded(chatModel: ChatModel, timeout: Long = 30_000) { + // Still decrypting database + if (chatModel.chatRunning.value == null) { + val step = 50L + for (i in 0..(timeout / step)) { + if (chatModel.chatRunning.value == true || chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.Step1_SimpleXInfo) { + break + } + delay(step) + } + } + } + + private fun hideSecrets(cItem: ChatItem): String { + val md = cItem.formattedText + return if (md != null) { + var res = "" + for (ft in md) { + res += if (ft.format is Format.Secret) "..." else ft.text + } + res + } else { + cItem.text + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt new file mode 100644 index 0000000000..84ffdb6fd7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -0,0 +1,28 @@ +package chat.simplex.common.platform + +import chat.simplex.common.model.NotificationsMode + +interface PlatformInterface { + suspend fun androidServiceStart() {} + fun androidServiceSafeStop() {} + fun androidNotificationsModeChanged(mode: NotificationsMode) {} + fun androidChatStartedAfterBeingOff() {} + fun androidChatStopped() {} + fun androidChatInitializedAndStarted() {} + fun androidIsBackgroundCallAllowed(): Boolean = true + suspend fun androidAskToAllowBackgroundCalls(): Boolean = true +} +/** + * Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them. + * This means that we can not call code from `android` directory via code from `common/androidMain` directory. So this is a way to do it: + * - we specify interface that should be implemented by platforms + * - platforms made its implementation by assigning it to this variable at runtime + * - common code calls this variable and everything works as expected. + * + * Functions that expected to be used on only one platform, should be prefixed with platform name, like androidSomething. It helps + * to identify it's use-case. Easy to understand that it is only needed on one specific platform. Functions without prefixes are used on + * more than one platform. + * + * See [SimplexApp] and [AppCommon.desktop] for re-assigning of this var + * */ +var platform: PlatformInterface = object : PlatformInterface {} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt new file mode 100644 index 0000000000..95b6a73ca4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt @@ -0,0 +1,18 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.text.TextStyle +import chat.simplex.common.views.chat.ComposeState + +@Composable +expect fun PlatformTextField( + composeState: MutableState<ComposeState>, + sendMsgEnabled: Boolean, + textStyle: MutableState<TextStyle>, + showDeleteTextButton: MutableState<Boolean>, + userIsObserver: Boolean, + onMessageChange: (String) -> Unit, + onUpArrow: () -> Unit, + onDone: () -> Unit, +) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt new file mode 100644 index 0000000000..0e0f769487 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -0,0 +1,42 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.MutableState +import chat.simplex.common.model.* +import kotlinx.coroutines.CoroutineScope + +interface RecorderInterface { + companion object { + // Allows to stop the recorder from outside without having the recorder in a variable + var stopRecording: (() -> Unit)? = null + val extension: String = "m4a" + } + fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String + fun stop(): Int +} + +expect class RecorderNative(): RecorderInterface + +interface AudioPlayerInterface { + fun play( + fileSource: CryptoFile, + audioPlaying: MutableState<Boolean>, + progress: MutableState<Int>, + duration: MutableState<Int>, + resetOnEnd: Boolean, + ) + fun stop() + fun stop(item: ChatItem) + fun stop(fileName: String?) + fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) + fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) + fun duration(unencryptedFilePath: String): Int? +} + +expect object AudioPlayer: AudioPlayerInterface + +interface SoundPlayerInterface { + fun start(scope: CoroutineScope, sound: Boolean) + fun stop() +} + +expect object SoundPlayer: SoundPlayerInterface diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt new file mode 100644 index 0000000000..2ee668fb23 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt @@ -0,0 +1,33 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.StringResource + +@Composable +expect fun font(name: String, res: String, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal): Font + +expect fun StringResource.localized(): String + +// Non-@Composable implementation +expect fun isInNightMode(): Boolean + +expect val settings: Settings +expect val settingsThemes: Settings + +enum class WindowOrientation { + UNDEFINED, PORTRAIT, LANDSCAPE +} + +expect fun windowOrientation(): WindowOrientation + +@Composable +expect fun windowWidth(): Dp + +expect fun desktopExpandWindowToWidth(width: Dp) + +expect fun isRtl(text: CharSequence): Boolean diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt new file mode 100644 index 0000000000..72bb3caaac --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Share.kt @@ -0,0 +1,10 @@ +package chat.simplex.common.platform + +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.UriHandler +import chat.simplex.common.model.CryptoFile + +expect fun UriHandler.sendEmail(subject: String, body: CharSequence) + +expect fun ClipboardManager.shareText(text: String) +expect fun shareFile(text: String, fileSource: CryptoFile) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt new file mode 100644 index 0000000000..38629f038b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/UI.kt @@ -0,0 +1,16 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* +import chat.simplex.common.views.helpers.KeyboardState + +expect fun showToast(text: String, timeout: Long = 2500L) + +@Composable +expect fun LockToCurrentOrientationUntilDispose() + +@Composable +expect fun LocalMultiplatformView(): Any? + +@Composable +expect fun getKeyboardState(): State<KeyboardState> +expect fun hideKeyboard(view: Any?) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt new file mode 100644 index 0000000000..5c3b50bbde --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/VideoPlayer.kt @@ -0,0 +1,66 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.MutableState +import androidx.compose.ui.graphics.ImageBitmap +import java.net.URI + +interface VideoPlayerInterface { + data class PreviewAndDuration(val preview: ImageBitmap?, val duration: Long?, val timestamp: Long) + + val uri: URI + val gallery: Boolean + val soundEnabled: MutableState<Boolean> + val brokenVideo: MutableState<Boolean> + val videoPlaying: MutableState<Boolean> + val progress: MutableState<Long> + val duration: MutableState<Long> + val preview: MutableState<ImageBitmap> + + fun stop() + fun play(resetOnEnd: Boolean) + fun enableSound(enable: Boolean): Boolean + fun release(remove: Boolean) +} + +expect class VideoPlayer( + uri: URI, + gallery: Boolean, + defaultPreview: ImageBitmap, + defaultDuration: Long, + soundEnabled: Boolean +): VideoPlayerInterface + +object VideoPlayerHolder { + val players: MutableMap<Pair<URI, Boolean>, VideoPlayer> = mutableMapOf() + val previewsAndDurations: MutableMap<URI, VideoPlayerInterface.PreviewAndDuration> = mutableMapOf() + + fun getOrCreate( + uri: URI, + gallery: Boolean, + defaultPreview: ImageBitmap, + defaultDuration: Long, + soundEnabled: Boolean + ): VideoPlayer = + players.getOrPut(uri to gallery) { VideoPlayer(uri, gallery, defaultPreview, defaultDuration, soundEnabled) } + + fun enableSound(enable: Boolean, fileName: String?, gallery: Boolean): Boolean = + player(fileName, gallery)?.enableSound(enable) == true + + private fun player(fileName: String?, gallery: Boolean): VideoPlayer? { + fileName ?: return null + return players.values.firstOrNull { player -> player.uri.path?.endsWith(fileName) == true && player.gallery == gallery } + } + + fun release(uri: URI, gallery: Boolean, remove: Boolean) = + player(uri.path, gallery)?.release(remove).run { } + + fun stopAll() { + players.values.forEach { it.stop() } + } + + fun releaseAll() { + players.values.forEach { it.release(false) } + players.clear() + previewsAndDurations.clear() + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt similarity index 96% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Color.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index 84686d7482..a01fc9de51 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.ui.theme +package chat.simplex.common.ui.theme import androidx.compose.ui.graphics.Color diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Shape.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Shape.kt similarity index 87% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Shape.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Shape.kt index 79ab4eead4..293257c095 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Shape.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Shape.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.ui.theme +package chat.simplex.common.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.Shapes diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Theme.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 0258ccbea0..6af3156ca2 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -1,22 +1,19 @@ -package chat.simplex.app.ui.theme +package chat.simplex.common.ui.theme -import android.app.UiModeManager -import android.content.Context import androidx.compose.foundation.background -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.views.helpers.* -import chat.simplex.res.MR +import chat.simplex.common.model.ChatController +import chat.simplex.common.platform.isInNightMode +import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import chat.simplex.res.MR enum class DefaultTheme { SYSTEM, LIGHT, DARK, SIMPLEX; @@ -193,6 +190,11 @@ val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_BOTTOM_PADDING = 48.dp val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp +val DEFAULT_START_MODAL_WIDTH = 388.dp +val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp +val DEFAULT_END_MODAL_WIDTH = 388.dp +val DEFAULT_MAX_IMAGE_WIDTH = 500.dp + val DarkColorPalette = darkColors( primary = SimplexBlue, // If this value changes also need to update #0088ff in string resource files primaryVariant = SimplexBlue, @@ -254,13 +256,18 @@ val SimplexColorPaletteApp = AppColors( val CurrentColors: MutableStateFlow<ThemeManager.ActiveTheme> = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) -// Non-@Composable implementation -private fun isInNightMode() = - (SimplexApp.context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager).nightMode == UiModeManager.MODE_NIGHT_YES - @Composable fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight +expect fun isSystemInDarkTheme(): Boolean + +fun reactOnDarkThemeChanges(isDark: Boolean) { + if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) { + // Change active colors from light to dark and back based on system theme + ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark) + } +} + @Composable fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { LaunchedEffect(darkTheme) { @@ -270,10 +277,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { } val systemDark = isSystemInDarkTheme() LaunchedEffect(systemDark) { - if (SimplexApp.context.chatModel.controller.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == systemDark) { - // Change active colors from light to dark and back based on system theme - ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, systemDark) - } + reactOnDarkThemeChanges(systemDark) } val theme by CurrentColors.collectAsState() MaterialTheme( diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 03d4af64c6..4a7521efb8 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,18 +1,21 @@ -package chat.simplex.app.ui.theme +package chat.simplex.common.ui.theme import androidx.compose.material.Colors import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.AppPreferences -import chat.simplex.app.views.helpers.generalGetString +import androidx.compose.ui.text.font.FontFamily import chat.simplex.res.MR +import chat.simplex.common.model.AppPreferences +import chat.simplex.common.model.ChatController +import chat.simplex.common.views.helpers.generalGetString + +// https://github.com/rsms/inter +// I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt +expect val Inter: FontFamily +expect val EmojiFont: FontFamily object ThemeManager { - private val appPrefs: AppPreferences by lazy { - SimplexApp.context.chatModel.controller.appPrefs - } + private val appPrefs: AppPreferences = ChatController.appPrefs data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Type.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt similarity index 68% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Type.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt index a67a14d0c1..9acfffb3ac 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/ui/theme/Type.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Type.kt @@ -1,20 +1,9 @@ -package chat.simplex.app.ui.theme +package chat.simplex.common.ui.theme import androidx.compose.material.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.unit.sp -import chat.simplex.res.MR - -// https://github.com/rsms/inter -val Inter: FontFamily = FontFamily( - Font(MR.fonts.Inter.regular.fontResourceId), - Font(MR.fonts.Inter.italic.fontResourceId, style = FontStyle.Italic), - Font(MR.fonts.Inter.bold.fontResourceId, FontWeight.Bold), - Font(MR.fonts.Inter.semibold.fontResourceId, FontWeight.SemiBold), - Font(MR.fonts.Inter.medium.fontResourceId, FontWeight.Medium), - Font(MR.fonts.Inter.light.fontResourceId, FontWeight.Light) -) // Set of Material typography styles to start with val Typography = Typography( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/Preview.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/Preview.kt new file mode 100644 index 0000000000..e83db99520 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/Preview.kt @@ -0,0 +1,7 @@ +package androidx.compose.desktop.ui.tooling.preview + +@Retention(AnnotationRetention.SOURCE) +@Target( + AnnotationTarget.FUNCTION +) +annotation class Preview diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/SplashView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt similarity index 94% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/SplashView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt index 9491ea75e0..cf9a8dfb62 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/SplashView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/SplashView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views +package chat.simplex.common.views import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.MaterialTheme diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt similarity index 82% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/TerminalView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index 264c243ebd..e471341669 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views +package chat.simplex.common.views -import android.content.res.Configuration -import androidx.activity.compose.BackHandler +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -9,32 +8,37 @@ import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.* -import chat.simplex.app.views.helpers.* -import com.google.accompanist.insets.ProvideWindowInsets -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* @Composable fun TerminalView(chatModel: ChatModel, close: () -> Unit) { val composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = false)) } + val close = { + close() + if (appPlatform.isDesktop) { + ModalManager.center.closeModals() + } + } BackHandler(onBack = { close() }) - TerminalLayout( - remember { chatModel.terminalItems }, - composeState, - sendCommand = { sendCommand(chatModel, composeState) }, - close - ) + TerminalLayout( + remember { chatModel.terminalItems }, + composeState, + sendCommand = { sendCommand(chatModel, composeState) }, + close + ) } private fun sendCommand(chatModel: ChatModel, composeState: MutableState<ComposeState>) { @@ -42,7 +46,7 @@ private fun sendCommand(chatModel: ChatModel, composeState: MutableState<Compose val prefPerformLA = chatModel.controller.appPrefs.performLA.get() val s = composeState.value.message if (s.startsWith("/sql") && (!prefPerformLA || !developerTools)) { - val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.СommandError("Failed reading: empty"))) + val resp = CR.ChatCmdError(null, ChatError.ChatErrorChat(ChatErrorType.CommandError("Failed reading: empty"))) chatModel.addTerminalItem(TerminalItem.cmd(CC.Console(s))) chatModel.addTerminalItem(TerminalItem.resp(resp)) composeState.value = ComposeState(useLinkPreviews = false) @@ -81,6 +85,8 @@ fun TerminalLayout( recState = remember { mutableStateOf(RecordingState.NotStarted) }, isDirectChat = false, liveMessageAlertShown = SharedPreference(get = { false }, set = {}), + sendMsgEnabled = true, + nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = false, userIsObserver = false, @@ -89,6 +95,7 @@ fun TerminalLayout( sendMessage = { sendCommand() }, sendLiveMessage = null, updateLiveMessage = null, + editPrevMessage = {}, onMessageChange = ::onMessageChange, textStyle = textStyle ) @@ -117,7 +124,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } val reversedTerminalItems by remember { derivedStateOf { terminalItems.reversed().toList() } } - val context = LocalContext.current + val clipboard = LocalClipboardManager.current LazyColumn(state = listState, reverseLayout = true) { items(reversedTerminalItems) { item -> Text( @@ -128,7 +135,7 @@ fun TerminalLog(terminalItems: List<TerminalItem>) { modifier = Modifier .fillMaxWidth() .clickable { - ModalManager.shared.showModal(endButtons = { ShareButton { shareText(item.details) } }) { + ModalManager.start.showModal(endButtons = { ShareButton { clipboard.shareText(item.details) } }) { SelectionContainer(modifier = Modifier.verticalScroll(rememberScrollState())) { Text(item.details, modifier = Modifier.padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING)) } @@ -139,12 +146,11 @@ fun TerminalLog(terminalItems: List<TerminalItem>) { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewTerminalLayout() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt similarity index 79% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/WelcomeView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index d273c17dbd..13ce16d0a7 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views +package chat.simplex.common.views import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -22,13 +22,14 @@ import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.Profile -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.OnboardingStage -import chat.simplex.app.views.onboarding.ReadableText -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.Profile +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.navigationBarsWithImePadding +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -88,14 +89,20 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) { icon = painterResource(MR.images.ic_arrow_back_ios_new), textDecoration = TextDecoration.None, fontWeight = FontWeight.Medium - ) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo } + ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } } Spacer(Modifier.fillMaxWidth().weight(1f)) val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value) val createModifier: Modifier val createColor: Color if (enabled) { - createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp) + createModifier = Modifier.clickable { + if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { + createProfileInProfiles(chatModel, displayName.value, fullName.value, close) + } else { + createProfileOnboarding(chatModel, displayName.value, fullName.value, close) + } + }.padding(8.dp) createColor = MaterialTheme.colors.primary } else { createModifier = Modifier.padding(8.dp) @@ -116,7 +123,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) { } } -fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { +fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { withApi { val user = chatModel.controller.apiCreateActiveUser( Profile(displayName, fullName, null) @@ -125,16 +132,32 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c if (chatModel.users.isEmpty()) { chatModel.controller.startChat(user) chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) - chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress } else { val users = chatModel.controller.listUsers() chatModel.users.clear() chatModel.users.addAll(users) chatModel.controller.getUserChatData() + close() + } + } +} + +fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) { + withApi { + chatModel.controller.apiCreateActiveUser( + Profile(displayName, fullName, null) + ) ?: return@withApi + val onboardingStage = chatModel.controller.appPrefs.onboardingStage + if (chatModel.users.isEmpty()) { + onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get()) { + OnboardingStage.Step2_5_SetupDatabasePassphrase + } else { + OnboardingStage.Step3_CreateSimpleXAddress + }) + } else { // the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen, // this will get it unstuck. - chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) - chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete + onboardingStage.set(OnboardingStage.OnboardingComplete) close() } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt similarity index 79% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallManager.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt index b87f996f8e..fe4da718a8 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt @@ -1,10 +1,10 @@ -package chat.simplex.app.views.call +package chat.simplex.common.views.call -import android.util.Log -import chat.simplex.app.TAG -import chat.simplex.app.model.ChatModel -import chat.simplex.app.views.helpers.ModalManager -import chat.simplex.app.views.helpers.withApi +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.withApi +import chat.simplex.common.views.helpers.withBGApi +import chat.simplex.common.views.usersettings.showInDevelopingAlert import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.minutes @@ -16,16 +16,20 @@ class CallManager(val chatModel: ChatModel) { if (invitation.user.showNotifications) { if (Clock.System.now() - invitation.callTs <= 3.minutes) { activeCallInvitation.value = invitation - controller.ntfManager.notifyCallInvitation(invitation) + ntfManager.notifyCallInvitation(invitation) } else { val contact = invitation.contact - controller.ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) + ntfManager.displayNotification(user = invitation.user, chatId = contact.id, displayName = contact.displayName, msgText = invitation.callTypeText) } } } } fun acceptIncomingCall(invitation: RcvCallInvitation) { + if (appPlatform.isDesktop) { + return showInDevelopingAlert() + } + val call = chatModel.activeCall.value if (call == null) { justAcceptIncomingCall(invitation = invitation) @@ -48,7 +52,7 @@ class CallManager(val chatModel: ChatModel) { contact = invitation.contact, callState = CallState.InvitationAccepted, localMedia = invitation.callType.media, - sharedKey = invitation.sharedKey + sharedKey = invitation.sharedKey, ) showCallView.value = true val useRelay = controller.appPrefs.webrtcPolicyRelay.get() @@ -63,7 +67,7 @@ class CallManager(val chatModel: ChatModel) { callInvitations.remove(invitation.contact.id) if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { activeCallInvitation.value = null - controller.ntfManager.cancelCallNotification() + ntfManager.cancelCallNotification() } } } @@ -89,7 +93,7 @@ class CallManager(val chatModel: ChatModel) { callInvitations.remove(invitation.contact.id) if (invitation.contact.id == activeCallInvitation.value?.contact?.id) { activeCallInvitation.value = null - controller.ntfManager.cancelCallNotification() + ntfManager.cancelCallNotification() } withApi { if (!controller.apiRejectCall(invitation.contact)) { @@ -102,7 +106,7 @@ class CallManager(val chatModel: ChatModel) { fun reportCallRemoteEnded(invitation: RcvCallInvitation) { if (chatModel.activeCallInvitation.value?.contact?.id == invitation.contact.id) { chatModel.activeCallInvitation.value = null - chatModel.controller.ntfManager.cancelCallNotification() + ntfManager.cancelCallNotification() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt new file mode 100644 index 0000000000..2f4ffbb836 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallView.kt @@ -0,0 +1,6 @@ +package chat.simplex.common.views.call + +import androidx.compose.runtime.Composable + +@Composable +expect fun ActiveCallView() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt similarity index 85% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 532ecda9c6..447236286f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -1,5 +1,6 @@ -package chat.simplex.app.views.call +package chat.simplex.common.views.call +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -10,34 +11,31 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ProfileImage -import chat.simplex.app.views.usersettings.ProfilePreview +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.ProfileImage +import chat.simplex.common.views.usersettings.ProfilePreview +import chat.simplex.common.platform.ntfManager +import chat.simplex.common.platform.SoundPlayer import chat.simplex.res.MR import kotlinx.datetime.Clock @Composable fun IncomingCallAlertView(invitation: RcvCallInvitation, chatModel: ChatModel) { val cm = chatModel.callManager - val cxt = LocalContext.current val scope = rememberCoroutineScope() - LaunchedEffect(true) { SoundPlayer.shared.start(scope, sound = !chatModel.showCallView.value) } - DisposableEffect(true) { onDispose { SoundPlayer.shared.stop() } } + LaunchedEffect(true) { SoundPlayer.start(scope, sound = !chatModel.showCallView.value) } + DisposableEffect(true) { onDispose { SoundPlayer.stop() } } IncomingCallAlertLayout( invitation, chatModel, rejectCall = { cm.endCall(invitation = invitation) }, ignoreCall = { chatModel.activeCallInvitation.value = null - chatModel.controller.ntfManager.cancelCallNotification() + ntfManager.cancelCallNotification() }, acceptCall = { cm.acceptIncomingCall(invitation = invitation) } ) @@ -114,7 +112,7 @@ fun PreviewIncomingCallAlertLayout() { sharedKey = null, callTs = Clock.System.now() ), - chatModel = SimplexApp.context.chatModel, + chatModel = ChatModel, rejectCall = {}, ignoreCall = {}, acceptCall = {} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/WebRTC.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index cec29c94fe..3e14414fc7 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -1,11 +1,9 @@ -package chat.simplex.app.views.call +package chat.simplex.common.views.call import androidx.compose.runtime.Composable import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.* -import chat.simplex.app.model.Contact -import chat.simplex.app.model.User -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.model.* import chat.simplex.res.MR import kotlinx.datetime.Instant import kotlinx.serialization.SerialName @@ -214,7 +212,7 @@ fun parseRTCIceServers(servers: List<String>): List<RTCIceServer>? { } fun getIceServers(): List<RTCIceServer>? { - val value = SimplexApp.context.chatController.appPrefs.webrtcIceServers.get() ?: return null + val value = ChatController.appPrefs.webrtcIceServers.get() ?: return null val servers: List<String> = value.split("\n") return parseRTCIceServers(servers) } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt similarity index 80% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 8e4fdb5337..5fcb90c1c9 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -1,15 +1,15 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat import InfoRow import InfoRowEllipsis import SectionBottomSpacer import SectionDividerSpaced import SectionItemView +import SectionItemViewSpaceBetween import SectionSpacer import SectionTextFooter import SectionView -import android.widget.Toast -import androidx.activity.compose.BackHandler +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.* @@ -24,21 +24,22 @@ import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chatlist.updateChatSettings -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCode -import chat.simplex.app.views.usersettings.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.chatlist.updateChatSettings import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import kotlinx.datetime.Clock @Composable @@ -52,15 +53,16 @@ fun ChatInfoView( close: () -> Unit, ) { BackHandler(onBack = close) - val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } - val currentUser = chatModel.currentUser.value - val connStats = remember { mutableStateOf(connectionStats) } + val contact = rememberUpdatedState(contact).value + val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } } + val currentUser = remember { chatModel.currentUser }.value + val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && currentUser != null) { - val contactNetworkStatus = remember(chatModel.networkStatuses.toMap()) { + val contactNetworkStatus = remember(chatModel.networkStatuses.toMap(), contact) { mutableStateOf(chatModel.contactNetworkStatus(contact)) } - val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } + val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } ChatInfoLayout( chat, contact, @@ -83,7 +85,7 @@ fun ChatInfoView( setContactAlias(chat.chatInfo.apiId, it, chatModel) }, openPreferences = { - ModalManager.shared.showCustomModal { close -> + ModalManager.end.showCustomModal { close -> val user = chatModel.currentUser.value if (user != null) { ContactPreferencesView(chatModel, user, contact.contactId, close) @@ -138,7 +140,7 @@ fun ChatInfoView( }) }, verifyClicked = { - ModalManager.shared.showModalCloseable { close -> + ModalManager.end.showModalCloseable { close -> remember { derivedStateOf { (chatModel.getContactChat(contact.contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct -> VerifyCodeView( ct.displayName, @@ -204,8 +206,11 @@ fun deleteContactDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId) if (r) { chatModel.removeChat(chatInfo.id) - chatModel.chatId.value = null - chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) close?.invoke() } } @@ -224,7 +229,7 @@ fun clearChatDialog(chatInfo: ChatInfo, chatModel: ChatModel, close: (() -> Unit val updatedChatInfo = chatModel.controller.apiClearChat(chatInfo.chatType, chatInfo.apiId) if (updatedChatInfo != null) { chatModel.clearChat(updatedChatInfo) - chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id) + ntfManager.cancelNotificationsForChat(chatInfo.id) close?.invoke() } } @@ -240,7 +245,7 @@ fun ChatInfoLayout( currentUser: User, sendReceipts: State<SendReceipts>, setSendReceipts: (SendReceipts) -> Unit, - connStats: MutableState<ConnectionStats?>, + connStats: State<ConnectionStats?>, contactNetworkStatus: NetworkStatus, customUserProfile: Profile?, localAlias: String, @@ -257,10 +262,15 @@ fun ChatInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + KeyChangeEffect(chat.id) { + scope.launch { scrollState.scrollTo(0) } + } Column( Modifier .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) ) { Row( Modifier.fillMaxWidth(), @@ -269,69 +279,79 @@ fun ChatInfoLayout( ChatInfoHeader(chat.chatInfo, contact) } - LocalAliasEditor(localAlias, updateValue = onLocalAliasChanged) + LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) SectionSpacer() if (customUserProfile != null) { SectionView(generalGetString(MR.strings.incognito).uppercase()) { - InfoRow(generalGetString(MR.strings.incognito_random_profile), customUserProfile.chatViewName) + SectionItemViewSpaceBetween { + Text(generalGetString(MR.strings.incognito_random_profile)) + Text(customUserProfile.chatViewName, color = Indigo) + } } SectionDividerSpaced() } - SectionView { - if (connectionCode != null) { - VerifyCodeButton(contact.verified, verifyClicked) + if (contact.ready) { + SectionView { + if (connectionCode != null) { + VerifyCodeButton(contact.verified, verifyClicked) + } + ContactPreferencesButton(openPreferences) + SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) + if (cStats != null && cStats.ratchetSyncAllowed) { + SynchronizeConnectionButton(syncContactConnection) + } + // } else if (developerTools) { + // SynchronizeConnectionButtonForce(syncContactConnectionForce) + // } } - ContactPreferencesButton(openPreferences) - SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) - if (cStats != null && cStats.ratchetSyncAllowed) { - SynchronizeConnectionButton(syncContactConnection) - } -// } else if (developerTools) { -// SynchronizeConnectionButtonForce(syncContactConnectionForce) -// } + SectionDividerSpaced() } - SectionDividerSpaced() if (contact.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { shareText(contact.contactLink) } + val clipboard = LocalClipboardManager.current + ShareAddressButton { clipboard.shareText(contact.contactLink) } SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) } SectionDividerSpaced() } - SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { - SectionItemView({ - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.network_status), - contactNetworkStatus.statusExplanation - )}) { - NetworkStatusRow(contactNetworkStatus) - } - if (cStats != null) { - SwitchAddressButton( - disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited, - switchAddress = switchContactAddress - ) - if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) { - AbortSwitchAddressButton( - disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited, - abortSwitchAddress = abortSwitchContactAddress + if (contact.ready) { + SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { + SectionItemView({ + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.network_status), + contactNetworkStatus.statusExplanation ) + }) { + NetworkStatusRow(contactNetworkStatus) } - val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer } - if (rcvServers.isNotEmpty()) { - SimplexServers(stringResource(MR.strings.receiving_via), rcvServers) - } - val sndServers = cStats.sndQueuesInfo.map { it.sndServer } - if (sndServers.isNotEmpty()) { - SimplexServers(stringResource(MR.strings.sending_via), sndServers) + if (cStats != null) { + SwitchAddressButton( + disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited, + switchAddress = switchContactAddress + ) + if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) { + AbortSwitchAddressButton( + disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited, + abortSwitchAddress = abortSwitchContactAddress + ) + } + val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer } + if (rcvServers.isNotEmpty()) { + SimplexServers(stringResource(MR.strings.receiving_via), rcvServers) + } + val sndServers = cStats.sndQueuesInfo.map { it.sndServer } + if (sndServers.isNotEmpty()) { + SimplexServers(stringResource(MR.strings.sending_via), sndServers) + } } } + SectionDividerSpaced() } - SectionDividerSpaced() + SectionView { ClearChatButton(clearChat) DeleteContactButton(deleteContact) @@ -390,13 +410,17 @@ fun ChatInfoHeader(cInfo: ChatInfo, contact: Contact) { @Composable fun LocalAliasEditor( + chatId: String, initialValue: String, center: Boolean = true, leadingIcon: Boolean = false, focus: Boolean = false, updateValue: (String) -> Unit ) { - var value by rememberSaveable { mutableStateOf(initialValue) } + val state = remember(chatId) { + mutableStateOf(TextFieldValue(initialValue)) + } + var updatedValueAtLeastOnce = remember { false } val modifier = if (center) Modifier.padding(horizontal = if (!leadingIcon) DEFAULT_PADDING else 0.dp).widthIn(min = 100.dp) else @@ -404,7 +428,7 @@ fun LocalAliasEditor( Row(Modifier.fillMaxWidth(), horizontalArrangement = if (center) Arrangement.Center else Arrangement.Start) { DefaultBasicTextField( modifier, - value, + state, { Text( generalGetString(MR.strings.text_field_set_contact_placeholder), @@ -417,23 +441,27 @@ fun LocalAliasEditor( } else null, color = MaterialTheme.colors.secondary, focus = focus, - textStyle = TextStyle.Default.copy(textAlign = if (value.isEmpty() || !center) TextAlign.Start else TextAlign.Center), - keyboardActions = KeyboardActions(onDone = { updateValue(value) }) + textStyle = TextStyle.Default.copy(textAlign = if (state.value.text.isEmpty() || !center) TextAlign.Start else TextAlign.Center), + keyboardActions = KeyboardActions(onDone = { updateValue(state.value.text) }) ) { - value = it + state.value = it + updatedValueAtLeastOnce = true } } - LaunchedEffect(Unit) { - snapshotFlow { value } + LaunchedEffect(chatId) { + var prevValue = state.value + snapshotFlow { state.value } + .distinctUntilChanged() .onEach { delay(500) } // wait a little after every new character, don't emit until user stops typing .conflate() // get the latest value - .filter { it == value } // don't process old ones + .filter { it == state.value && it != prevValue } // don't process old ones .collect { - updateValue(value) + updateValue(it.text) + prevValue = it } } - DisposableEffect(Unit) { - onDispose { updateValue(value) } // just in case snapshotFlow will be canceled when user presses Back too fast + DisposableEffect(chatId) { + onDispose { if (updatedValueAtLeastOnce) updateValue(state.value.text) } // just in case snapshotFlow will be canceled when user presses Back too fast } } @@ -490,7 +518,7 @@ fun SimplexServers(text: String, servers: List<String>) { val clipboardManager: ClipboardManager = LocalClipboardManager.current InfoRowEllipsis(text, info) { clipboardManager.setText(AnnotatedString(servers.joinToString(separator = ","))) - Toast.makeText(SimplexApp.context, generalGetString(MR.strings.copied), Toast.LENGTH_SHORT).show() + showToast(generalGetString(MR.strings.copied)) } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt similarity index 62% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index 4d7cc4b8ea..53a5fd9d62 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -1,8 +1,9 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat import InfoRow import SectionBottomSpacer import SectionDividerSpaced +import SectionItemView import SectionView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -12,32 +13,37 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.CurrentColors -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.chat.item.ItemAction -import chat.simplex.app.views.chat.item.MarkdownText -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chat.item.MarkdownText +import chat.simplex.common.views.helpers.* +import chat.simplex.common.platform.shareText +import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource -enum class CIInfoTab { - History, Quote +sealed class CIInfoTab { + class Delivery(val memberDeliveryStatuses: List<MemberDeliveryStatus>): CIInfoTab() + object History: CIInfoTab() + class Quote(val quotedItem: CIQuote): CIInfoTab() } @Composable -fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { +fun ChatItemInfoView(chatModel: ChatModel, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { val sent = ci.chatDir.sent val appColors = CurrentColors.collectAsState().value.appColors val uriHandler = LocalUriHandler.current - val selection = remember { mutableStateOf(CIInfoTab.History) } + val selection = remember { mutableStateOf<CIInfoTab>(CIInfoTab.History) } @Composable fun TextBubble(text: String, formattedText: List<FormattedText>?, sender: String?, showMenu: MutableState<Boolean>) { @@ -67,6 +73,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { Box( Modifier.clip(RoundedCornerShape(18.dp)).background(itemColor).padding(bottom = 3.dp) .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) + .onRightClick { showMenu.value = true } ) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { TextBubble(text, ciVersion.formattedText, sender = null, showMenu) @@ -88,13 +95,14 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { } } if (text != "") { + val clipboard = LocalClipboardManager.current DefaultDropdownMenu(showMenu) { ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { - shareText(text) + clipboard.shareText(text) showMenu.value = false }) ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { - copyText(text) + clipboard.setText(AnnotatedString(text)) showMenu.value = false }) } @@ -112,6 +120,7 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { Box( Modifier.clip(RoundedCornerShape(18.dp)).background(quoteColor).padding(bottom = 3.dp) .combinedClickable(onLongClick = { showMenu.value = true }, onClick = {}) + .onRightClick { showMenu.value = true } ) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { TextBubble(text, qi.formattedText, sender = qi.sender(null), showMenu) @@ -126,13 +135,14 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { ) } if (text != "") { + val clipboard = LocalClipboardManager.current DefaultDropdownMenu(showMenu) { ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { - shareText(text) + clipboard.shareText(text) showMenu.value = false }) ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { - copyText(text) + clipboard.setText(AnnotatedString(text)) showMenu.value = false }) } @@ -209,55 +219,153 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { } } + @Composable + fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) { + SectionItemView( + padding = PaddingValues(horizontal = 0.dp) + ) { + ProfileImage(size = 36.dp, member.image) + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + Text( + member.chatViewName, + modifier = Modifier.weight(10f, fill = true), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) + var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp)) + val info = status.statusInto + if (info != null) { + modifier = modifier.clickable { + AlertManager.shared.showAlertMsg( + title = info.first, + text = info.second + ) + } + } + Box(modifier, contentAlignment = Alignment.Center) { + if (statusIcon != null) { + val (icon, statusColor) = statusIcon + Icon( + painterResource(icon), + contentDescription = null, + tint = statusColor + ) + } else { + Icon( + painterResource(MR.images.ic_more_horiz), + contentDescription = null, + tint = CurrentColors.value.colors.secondary + ) + } + } + } + } + + @Composable + fun DeliveryTab(memberDeliveryStatuses: List<MemberDeliveryStatus>) { + Column(Modifier.fillMaxWidth().verticalScroll(rememberScrollState())) { + Details() + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) + val mss = membersStatuses(chatModel, memberDeliveryStatuses) + if (mss.isNotEmpty()) { + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) + mss.forEach { (member, status) -> + MemberDeliveryStatusView(member, status) + } + } + } else { + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(stringResource(MR.strings.no_info_on_delivery), color = MaterialTheme.colors.secondary) + } + } + } + SectionBottomSpacer() + } + } + @Composable fun tabTitle(tab: CIInfoTab): String { return when (tab) { - CIInfoTab.History -> stringResource(MR.strings.edit_history) - CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to) + is CIInfoTab.Delivery -> stringResource(MR.strings.delivery) + is CIInfoTab.History -> stringResource(MR.strings.edit_history) + is CIInfoTab.Quote -> stringResource(MR.strings.in_reply_to) } } fun tabIcon(tab: CIInfoTab): ImageResource { return when (tab) { - CIInfoTab.History -> MR.images.ic_history - CIInfoTab.Quote -> MR.images.ic_reply + is CIInfoTab.Delivery -> MR.images.ic_double_check + is CIInfoTab.History -> MR.images.ic_history + is CIInfoTab.Quote -> MR.images.ic_reply } } - Column { + fun numTabs(): Int { + var numTabs = 1 + if (ciInfo.memberDeliveryStatuses != null) { + numTabs += 1 + } if (ci.quotedItem != null) { + numTabs += 1 + } + return numTabs + } + + Column { + if (numTabs() > 1) { Column( Modifier .fillMaxHeight(), verticalArrangement = Arrangement.SpaceBetween ) { + LaunchedEffect(Unit) { + if (ciInfo.memberDeliveryStatuses != null) { + selection.value = CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses) + } + } Column(Modifier.weight(1f)) { - when (selection.value) { - CIInfoTab.History -> { + when (val sel = selection.value) { + is CIInfoTab.Delivery -> { + DeliveryTab(sel.memberDeliveryStatuses) + } + + is CIInfoTab.History -> { HistoryTab() } - CIInfoTab.Quote -> { - QuoteTab(ci.quotedItem) + is CIInfoTab.Quote -> { + QuoteTab(sel.quotedItem) } } } + val availableTabs = mutableListOf<CIInfoTab>() + if (ciInfo.memberDeliveryStatuses != null) { + availableTabs.add(CIInfoTab.Delivery(ciInfo.memberDeliveryStatuses)) + } + availableTabs.add(CIInfoTab.History) + if (ci.quotedItem != null) { + availableTabs.add(CIInfoTab.Quote(ci.quotedItem)) + } TabRow( - selectedTabIndex = selection.value.ordinal, + selectedTabIndex = availableTabs.indexOfFirst { it::class == selection.value::class }, backgroundColor = Color.Transparent, contentColor = MaterialTheme.colors.primary, ) { - CIInfoTab.values().forEachIndexed { index, it -> + availableTabs.forEach { ciInfoTab -> Tab( - selected = selection.value.ordinal == index, + selected = selection.value::class == ciInfoTab::class, onClick = { - selection.value = CIInfoTab.values()[index] + selection.value = ciInfoTab }, - text = { Text(tabTitle(it), fontSize = 13.sp) }, + text = { Text(tabTitle(ciInfoTab), fontSize = 13.sp) }, icon = { Icon( - painterResource(tabIcon(it)), - tabTitle(it) + painterResource(tabIcon(ciInfoTab)), + tabTitle(ciInfoTab) ) }, selectedContentColor = MaterialTheme.colors.primary, @@ -272,10 +380,18 @@ fun ChatItemInfoView(ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { } } -fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String { +private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List<MemberDeliveryStatus>): List<Pair<GroupMember, CIStatus>> { + return memberDeliveryStatuses.mapNotNull { mds -> + chatModel.groupMembers.firstOrNull { it.groupMemberId == mds.groupMemberId }?.let { mem -> + mem to mds.memberDeliveryStatus + } + } +} + +fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolean): String { val meta = ci.meta val sent = ci.chatDir.sent - val shareText = mutableListOf<String>(generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "") + val shareText = mutableListOf<String>("# " + generalGetString(if (sent) MR.strings.sent_message else MR.strings.received_message), "") shareText.add(String.format(generalGetString(MR.strings.share_text_sent_at), localTimestamp(meta.itemTs))) if (!ci.chatDir.sent) { @@ -305,7 +421,7 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea val qi = ci.quotedItem if (qi != null) { shareText.add("") - shareText.add(generalGetString(MR.strings.in_reply_to)) + shareText.add("## " + generalGetString(MR.strings.in_reply_to)) shareText.add("") val ts = localTimestamp(qi.sentAt) val sender = qi.sender(null) @@ -320,7 +436,7 @@ fun itemInfoShareText(ci: ChatItem, chatItemInfo: ChatItemInfo, devTools: Boolea val versions = chatItemInfo.itemVersions if (versions.isNotEmpty()) { shareText.add("") - shareText.add(generalGetString(MR.strings.edit_history)) + shareText.add("## " + generalGetString(MR.strings.edit_history)) versions.forEachIndexed { index, itemVersion -> val ts = localTimestamp(itemVersion.itemVersionTs) shareText.add("") diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt similarity index 71% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index ca64f5c8d4..596a7426ad 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1,10 +1,6 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat -import android.content.res.Configuration -import android.graphics.Bitmap -import android.net.Uri -import android.util.Log -import androidx.activity.compose.BackHandler +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* @@ -15,40 +11,39 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier +import androidx.compose.ui.* import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.* import androidx.compose.ui.unit.* -import androidx.core.content.FileProvider -import chat.simplex.app.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.call.* -import chat.simplex.app.views.chat.group.* -import chat.simplex.app.views.chat.item.* -import chat.simplex.app.views.chatlist.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.helpers.AppBarHeight -import com.google.accompanist.insets.ProvideWindowInsets -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.* +import chat.simplex.common.views.chat.group.* +import chat.simplex.common.views.chat.item.* +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.* +import chat.simplex.common.platform.AudioPlayer +import chat.simplex.common.views.usersettings.showInDevelopingAlert import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.datetime.Clock import java.io.File +import java.net.URI import kotlin.math.sign @Composable -fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { +fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) { val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) } val searchText = rememberSaveable { mutableStateOf("") } val user = chatModel.currentUser.value @@ -71,12 +66,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { launch { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() - .collect { - if (activeChat.value?.id != chatModel.chatId.value && chatModel.chatId.value != null) { - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatModel.chatId.value!!) - } + .filter { it != null && activeChat.value?.id != it } + .collect { chatId -> + // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly + // Also for situation when chatId changes after clicking in notification, etc + activeChat.value = chatModel.getChat(chatId!!) markUnreadChatAsRead(activeChat, chatModel) } } @@ -96,30 +90,42 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { } .distinctUntilChanged() // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions - .filter { it?.chatInfo != activeChat.value?.chatInfo && it != null } + .filter { it != null && it?.chatInfo != activeChat.value?.chatInfo } .collect { activeChat.value = it } } } - val view = LocalView.current - val context = LocalContext.current + val view = LocalMultiplatformView() if (activeChat.value == null || user == null) { chatModel.chatId.value = null + ModalManager.end.closeModals() } else { val chat = activeChat.value!! // We need to have real unreadCount value for displaying it inside top right button // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }?.chatStats?.unreadCount ?: 0 + chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 } } + val clipboard = LocalClipboardManager.current ChatLayout( chat, unreadCount, composeState, composeView = { - if (chat.chatInfo.sendMsgEnabled) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (chat.chatInfo is ChatInfo.Direct && !chat.chatInfo.contact.ready && !chat.chatInfo.contact.nextSendGrpInv) { + Text( + generalGetString(MR.strings.contact_connection_pending), + Modifier.padding(top = 4.dp), + fontSize = 14.sp, + color = MaterialTheme.colors.secondary + ) + } ComposeView( chatModel, chat, composeState, attachmentOption, showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } @@ -132,32 +138,51 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { searchText, useLinkPreviews = useLinkPreviews, linkMode = chatModel.simplexLinkMode.value, - chatModelIncognito = chatModel.incognito.value, back = { hideKeyboard(view) AudioPlayer.stop() chatModel.chatId.value = null }, info = { + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } hideKeyboard(view) withApi { + // The idea is to preload information before showing a modal because large groups can take time to load all members + var preloadedContactInfo: Pair<ConnectionStats, Profile?>? = null + var preloadedCode: String? = null + var preloadedLink: Pair<String, GroupMemberRole>? = null if (chat.chatInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) - val (_, code) = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId) - ModalManager.shared.showModalCloseable(true) { close -> - remember { derivedStateOf { (chatModel.getContactChat(chat.chatInfo.apiId)?.chatInfo as? ChatInfo.Direct)?.contact } }.value?.let { ct -> - ChatInfoView(chatModel, ct, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) - } - } + preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) + preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second } else if (chat.chatInfo is ChatInfo.Group) { setGroupMembers(chat.chatInfo.groupInfo, chatModel) - val link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) - var groupLink = link?.first - var groupLinkMemberRole = link?.second - ModalManager.shared.showModalCloseable(true) { close -> - GroupChatInfoView(chatModel, groupLink, groupLinkMemberRole, { - groupLink = it.first; - groupLinkMemberRole = it.second + preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) + } + ModalManager.end.showModalCloseable(true) { close -> + val chat = remember { activeChat }.value + if (chat?.chatInfo is ChatInfo.Direct) { + var contactInfo: Pair<ConnectionStats, Profile?>? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) + preloadedContactInfo = contactInfo + code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second + preloadedCode = code + } + ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) + } else if (chat?.chatInfo is ChatInfo.Group) { + var link: Pair<String, GroupMemberRole>? by remember(chat.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chat.id) { + setGroupMembers((chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) + preloadedLink = link + } + GroupChatInfoView(chatModel, link?.first, link?.second, { + link = it + preloadedLink = it }, close) } } @@ -169,17 +194,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) val stats = r?.second val (_, code) = if (member.memberActive) { - try { - chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) - } catch (e: Exception) { - Log.e(TAG, e.stackTraceToString()) - member to null - } + val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) + member to memCode?.second } else { member to null } setGroupMembers(groupInfo, chatModel) - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(true) { close -> remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem -> GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, close, close) } @@ -229,8 +251,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { } } }, - receiveFile = { fileId -> - withApi { chatModel.controller.receiveFile(user, fileId) } + receiveFile = { fileId, encrypted -> + withApi { chatModel.controller.receiveFile(user, fileId, encrypted) } }, cancelFile = { fileId -> withApi { chatModel.controller.cancelFile(user, fileId) } @@ -238,12 +260,17 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { joinGroup = { groupId -> withApi { chatModel.controller.apiJoinGroup(groupId) } }, - startCall = { media -> - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media) - chatModel.showCallView.value = true - chatModel.callCommand.value = WCallCommand.Capabilities + startCall = out@ { media -> + if (appPlatform.isDesktop) { + return@out showInDevelopingAlert() + } + withBGApi { + val cInfo = chat.chatInfo + if (cInfo is ChatInfo.Direct) { + chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media) + chatModel.showCallView.value = true + chatModel.callCommand.value = WCallCommand.Capabilities + } } }, acceptCall = { contact -> @@ -260,6 +287,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { chatModel.controller.allowFeatureToContact(contact, feature, param) } }, + openDirectChat = { contactId -> + withApi { + openDirectChat(contactId, chatModel) + } + }, updateContactStats = { contact -> withApi { val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) @@ -319,10 +351,14 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { withApi { val ciInfo = chatModel.controller.apiGetChatItemInfo(cInfo.chatType, cInfo.apiId, cItem.id) if (ciInfo != null) { - ModalManager.shared.showModal(endButtons = { ShareButton { - shareText(itemInfoShareText(cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) + if (chat.chatInfo is ChatInfo.Group) { + setGroupMembers(chat.chatInfo.groupInfo, chatModel) + } + ModalManager.end.closeModals() + ModalManager.end.showModal(endButtons = { ShareButton { + clipboard.shareText(itemInfoShareText(chatModel, cItem, ciInfo, chatModel.controller.appPrefs.developerTools.get())) } }) { - ChatItemInfoView(cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + ChatItemInfoView(chatModel, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) } } } @@ -331,14 +367,15 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { hideKeyboard(view) withApi { setGroupMembers(groupInfo, chatModel) - ModalManager.shared.showModalCloseable(true) { close -> - AddGroupMembersView(groupInfo, false, chatModel, close) + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(true) { close -> + AddGroupMembersView(groupInfo, false, chatModel, close) } } }, markRead = { range, unreadCountAfter -> chatModel.markChatItemsRead(chat.chatInfo, range, unreadCountAfter) - chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) + ntfManager.cancelNotificationsForChat(chat.id) withBGApi { chatModel.controller.apiChatRead( chat.chatInfo.chatType, @@ -357,6 +394,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: () -> Unit) { } }, onComposed, + developerTools = chatModel.controller.appPrefs.developerTools.get(), ) } } @@ -373,18 +411,18 @@ fun ChatLayout( searchValue: State<String>, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, - chatModelIncognito: Boolean, back: () -> Unit, info: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long) -> Unit, startCall: (CallMediaType) -> Unit, acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, + openDirectChat: (Long) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -397,12 +435,39 @@ fun ChatLayout( markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit, onSearchValueChanged: (String) -> Unit, - onComposed: () -> Unit, + onComposed: suspend (chatId: String) -> Unit, + developerTools: Boolean, ) { val scope = rememberCoroutineScope() + val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } Box( Modifier .fillMaxWidth() + .desktopOnExternalDrag( + enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, + onFiles = { paths -> + val uris = paths.map { URI.create(it) } + val groups = uris.groupBy { isImage(it) } + val images = groups[true] ?: emptyList() + val files = groups[false] ?: emptyList() + if (images.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(images, null) } + } else if (files.isNotEmpty()) { + composeState.processPickedFile(uris.first(), null) + } + }, + onImage = { + val tmpFile = File.createTempFile("image", ".bmp", tmpDir) + tmpFile.deleteOnExit() + chatModel.filesToDelete.add(tmpFile) + val uri = tmpFile.toURI() + CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(listOf(uri), null) } + }, + onText = { + // Need to parse HTML in order to correctly display the content + //composeState.value = composeState.value.copy(message = composeState.value.message + it) + }, + ) ) { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { ModalBottomSheetLayout( @@ -434,10 +499,10 @@ fun ChatLayout( ) { ChatItemsList( chat, unreadCount, composeState, chatItems, searchValue, - useLinkPreviews, linkMode, chatModelIncognito, showMemberInfo, loadPrevMessages, deleteMessage, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, + useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, setFloatingButton, onComposed, + setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, ) } } @@ -467,11 +532,13 @@ fun ChatInfoToolbar( showSearch = false } } - BackHandler(onBack = onBackClicked) + if (appPlatform.isAndroid) { + BackHandler(onBack = onBackClicked) + } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() menuItems.add { - ItemAction(stringResource(MR.strings.search_verb).capitalize(Locale.current), painterResource(MR.images.ic_search), onClick = { + ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { showMenu.value = false showSearch = true }) @@ -482,15 +549,22 @@ fun ChatInfoToolbar( IconButton({ showMenu.value = false startCall(CallMediaType.Audio) - }) { - Icon(painterResource(MR.images.ic_call_500), stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary) + }, + enabled = chat.chatInfo.contact.ready) { + Icon( + painterResource(MR.images.ic_call_500), + stringResource(MR.strings.icon_descr_more_button), + tint = if (chat.chatInfo.contact.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) } } - menuItems.add { - ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { - showMenu.value = false - startCall(CallMediaType.Video) - }) + if (chat.chatInfo.contact.ready) { + menuItems.add { + ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { + showMenu.value = false + startCall(CallMediaType.Video) + }) + } } } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) { barButtons.add { @@ -502,20 +576,22 @@ fun ChatInfoToolbar( } } } - val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } - menuItems.add { - ItemAction( - if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), - if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), - onClick = { - showMenu.value = false - // Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu - scope.launch { - delay(200) - changeNtfsState(!ntfsEnabled.value, ntfsEnabled) + if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready) || chat.chatInfo is ChatInfo.Group) { + val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + menuItems.add { + ItemAction( + if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + onClick = { + showMenu.value = false + // Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu + scope.launch { + delay(200) + changeNtfsState(!ntfsEnabled.value, ntfsEnabled) + } } - } - ) + ) + } } barButtons.add { @@ -525,7 +601,7 @@ fun ChatInfoToolbar( } DefaultTopAppBar( - navigationButton = { NavigationButtonBack(onBackClicked) }, + navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } }, title = { ChatInfoToolbarTitle(chat.chatInfo) }, onTitleClick = info, showSearch = showSearch, @@ -601,15 +677,15 @@ fun BoxWithConstraintsScope.ChatItemsList( searchValue: State<String>, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, - chatModelIncognito: Boolean, showMemberInfo: (GroupInfo, GroupMember) -> Unit, loadPrevMessages: (ChatInfo) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long) -> Unit, acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, + openDirectChat: (Long) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -620,7 +696,8 @@ fun BoxWithConstraintsScope.ChatItemsList( showItemDetails: (ChatInfo, ChatItem) -> Unit, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, setFloatingButton: (@Composable () -> Unit) -> Unit, - onComposed: () -> Unit, + onComposed: suspend (chatId: String) -> Unit, + developerTools: Boolean, ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -651,23 +728,23 @@ fun BoxWithConstraintsScope.ChatItemsList( scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } } } - LaunchedEffect(Unit) { + LaunchedEffect(chat.id) { var stopListening = false snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } .distinctUntilChanged() .filter { !stopListening } .collect { - onComposed() - stopListening = true + onComposed(chat.id) + stopListening = true } } DisposableEffectOnGone( whenGone = { - VideoPlayer.releaseAll() + VideoPlayerHolder.releaseAll() } ) LazyColumn(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) { - itemsIndexed(reversedChatItems, key = { _, item -> item.id}) { i, cItem -> + itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem -> CompositionLocalProvider( // Makes horizontal and vertical scrolling to coexist nicely. // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view @@ -709,28 +786,74 @@ fun BoxWithConstraintsScope.ChatItemsList( if (chat.chatInfo is ChatInfo.Group) { if (cItem.chatDir is CIDirection.GroupRcv) { val prevItem = if (i < reversedChatItems.lastIndex) reversedChatItems[i + 1] else null - val member = cItem.chatDir.groupMember - val showMember = showMemberImage(member, prevItem) - Row(Modifier.padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp).then(swipeableModifier)) { - if (showMember) { - Box( - Modifier - .clip(CircleShape) - .clickable { - showMemberInfo(chat.chatInfo.groupInfo, member) - } - ) { - MemberImage(member) + val nextItem = if (i - 1 >= 0) reversedChatItems[i - 1] else null + fun getConnectedMemberNames(): List<String> { + val ns = mutableListOf<String>() + var idx = i + while (idx < reversedChatItems.size) { + val m = reversedChatItems[idx].memberConnected + if (m != null) { + ns.add(m.displayName) + } else { + break + } + idx++ + } + return ns + } + if (cItem.memberConnected != null && nextItem?.memberConnected != null) { + // memberConnected events are aggregated at the last chat item in a row of such events, see ChatItemView + Box(Modifier.size(0.dp)) {} + } else { + val member = cItem.chatDir.groupMember + if (showMemberImage(member, prevItem)) { + Column( + Modifier + .padding(top = 8.dp) + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start + ) { + if (cItem.content.showMemberName) { + Text( + member.displayName, + Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + } + Row( + swipeableModifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Box( + Modifier + .clip(CircleShape) + .clickable { + showMemberInfo(chat.chatInfo.groupInfo, member) + } + ) { + MemberImage(member) + } + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools) + } + } + } else { + Row( + Modifier + .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) + .then(swipeableModifier) + ) { + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames, developerTools = developerTools) } - Spacer(Modifier.size(4.dp)) - } else { - Spacer(Modifier.size(42.dp)) } - ChatItemView(chat.chatInfo, cItem, composeState, provider, showMember = showMember, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) } } else { - Box(Modifier.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp).then(swipeableModifier)) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) + Box( + Modifier + .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) + .then(swipeableModifier) + ) { + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) } } } else { // direct message @@ -741,11 +864,11 @@ fun BoxWithConstraintsScope.ChatItemsList( end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, ).then(swipeableModifier) ) { - ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails) + ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) } } - if (cItem.isRcvNew) { + if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { scope.launch { delay(600) @@ -783,7 +906,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .collect { try { - if (listState.firstVisibleItemIndex == 0) { + if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) { listState.animateScrollToItem(0) } else { listState.animateScrollBy(scrollDistance) @@ -877,17 +1000,19 @@ fun BoxWithConstraintsScope.FloatingButtons( onLongClick = { showDropDown.value = true } ) - DefaultDropdownMenu(showDropDown, offset = DpOffset(maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { - ItemAction( - generalGetString(MR.strings.mark_read), - painterResource(MR.images.ic_check), - onClick = { - markRead( - CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), - bottomUnreadCount - ) - showDropDown.value = false - }) + Box { + DefaultDropdownMenu(showDropDown, offset = DpOffset(this@FloatingButtons.maxWidth - DEFAULT_PADDING, 24.dp + fabSize)) { + ItemAction( + generalGetString(MR.strings.mark_read), + painterResource(MR.images.ic_check), + onClick = { + markRead( + CC.ItemRange(minUnreadItemId, chatItems[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), + bottomUnreadCount + ) + showDropDown.value = false + }) + } } } @@ -922,9 +1047,11 @@ fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean { (prevItem.chatDir is CIDirection.GroupRcv && prevItem.chatDir.groupMember.groupMemberId != member.groupMemberId) } +val MEMBER_IMAGE_SIZE: Dp = 38.dp + @Composable fun MemberImage(member: GroupMember) { - ProfileImage(38.dp, member.memberProfile.image) + ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image) } @Composable @@ -1016,8 +1143,8 @@ private fun markUnreadChatAsRead(activeChat: MutableState<Chat?>, chatModel: Cha } sealed class ProviderMedia { - data class Image(val uri: Uri, val image: Bitmap): ProviderMedia() - data class Video(val uri: Uri, val preview: String): ProviderMedia() + data class Image(val data: ByteArray, val image: ImageBitmap): ProviderMedia() + data class Video(val uri: URI, val preview: String): ProviderMedia() } private fun providerForGallery( @@ -1054,17 +1181,17 @@ private fun providerForGallery( val item = item(internalIndex, initialChatId)?.second ?: return null return when (item.content.msgContent) { is MsgContent.MCImage -> { - val imageBitmap: Bitmap? = getLoadedImage(item.file) + val res = getLoadedImage(item.file) val filePath = getLoadedFilePath(item.file) - if (imageBitmap != null && filePath != null) { - val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) - ProviderMedia.Image(uri, imageBitmap) + if (res != null && filePath != null) { + val (imageBitmap: ImageBitmap, data: ByteArray) = res + ProviderMedia.Image(data, imageBitmap) } else null } is MsgContent.MCVideo -> { val filePath = getLoadedFilePath(item.file) if (filePath != null) { - val uri = FileProvider.getUriForFile(SimplexApp.context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) + val uri = getAppFileUri(filePath.substringAfterLast(File.separator)) ProviderMedia.Video(uri, (item.content.msgContent as MsgContent.MCVideo).image) } else null } @@ -1108,12 +1235,11 @@ private fun ViewConfiguration.bigTouchSlop(slop: Float = 50f) = object: ViewConf override val touchSlop: Float get() = slop } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatLayout() { SimpleXTheme { @@ -1152,18 +1278,18 @@ fun PreviewChatLayout() { searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, - chatModelIncognito = false, back = {}, info = {}, showMemberInfo = { _, _ -> }, loadPrevMessages = { _ -> }, deleteMessage = { _, _ -> }, - receiveFile = {}, + receiveFile = { _, _ -> }, cancelFile = {}, joinGroup = {}, startCall = {}, acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, + openDirectChat = { _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1177,11 +1303,12 @@ fun PreviewChatLayout() { changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, onComposed = {}, + developerTools = false, ) } } -@Preview(showBackground = true) +@Preview @Composable fun PreviewGroupChatLayout() { SimpleXTheme { @@ -1220,18 +1347,18 @@ fun PreviewGroupChatLayout() { searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, - chatModelIncognito = false, back = {}, info = {}, showMemberInfo = { _, _ -> }, loadPrevMessages = { _ -> }, deleteMessage = { _, _ -> }, - receiveFile = {}, + receiveFile = { _, _ -> }, cancelFile = {}, joinGroup = {}, startCall = {}, acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, + openDirectChat = { _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1245,6 +1372,7 @@ fun PreviewGroupChatLayout() { changeNtfsState = { _, _ -> }, onSearchValueChanged = {}, onComposed = {}, + developerTools = false, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt new file mode 100644 index 0000000000..20316dd524 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt @@ -0,0 +1,39 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun ComposeContextInvitingContactMemberView() { + val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + Row( + Modifier + .height(60.dp) + .fillMaxWidth() + .padding(top = 8.dp) + .background(sentColor), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(MR.images.ic_chat), + stringResource(MR.strings.button_send_direct_message), + modifier = Modifier + .padding(start = 12.dp, end = 8.dp) + .height(20.dp) + .width(20.dp), + tint = MaterialTheme.colors.secondary + ) + Text(generalGetString(MR.strings.compose_send_direct_message_to_connect)) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt index 8043b7c8e8..83076f885b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt @@ -1,3 +1,6 @@ +package chat.simplex.common.views.chat + +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -7,10 +10,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.* +import chat.simplex.common.ui.theme.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt similarity index 85% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt index 3c36a6e2af..906065f741 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -10,15 +10,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.UploadContent -import chat.simplex.app.views.helpers.base64ToBitmap +import chat.simplex.common.platform.base64ToBitmap import chat.simplex.res.MR +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.UploadContent @Composable fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { @@ -38,7 +36,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni val content = media.content[index] if (content is UploadContent.Video) { Box(contentAlignment = Alignment.Center) { - val imageBitmap = base64ToBitmap(item).asImageBitmap() + val imageBitmap = base64ToBitmap(item) Image( imageBitmap, "preview video", @@ -53,7 +51,7 @@ fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Uni ) } } else { - val imageBitmap = base64ToBitmap(item).asImageBitmap() + val imageBitmap = base64ToBitmap(item) Image( imageBitmap, "preview image", diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt similarity index 68% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index f0569c6661..c6e6ca7b7a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -1,22 +1,6 @@ @file:UseSerializers(UriSerializer::class) -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat -import ComposeFileView -import ComposeVoiceView -import android.Manifest -import android.app.Activity -import android.content.* -import android.content.pm.PackageManager -import android.graphics.* -import android.graphics.drawable.AnimatedImageDrawable -import android.net.Uri -import android.os.Build -import android.provider.MediaStore -import android.webkit.MimeTypeMap -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContract -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* @@ -26,21 +10,22 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.ImageBitmap import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.views.chat.item.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.Indigo +import chat.simplex.common.ui.theme.isSystemInDarkTheme +import chat.simplex.common.views.chat.item.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.* import java.io.File +import java.net.URI import java.nio.file.Files @Serializable @@ -49,7 +34,7 @@ sealed class ComposePreview { @Serializable class CLinkPreview(val linkPreview: LinkPreview?): ComposePreview() @Serializable class MediaPreview(val images: List<String>, val content: List<UploadContent>): ComposePreview() @Serializable data class VoicePreview(val voice: String, val durationMs: Int, val finished: Boolean): ComposePreview() - @Serializable class FilePreview(val fileName: String, val uri: Uri): ComposePreview() + @Serializable class FilePreview(val fileName: String, val uri: URI): ComposePreview() } @Serializable @@ -129,7 +114,7 @@ data class ComposeState( } val empty: Boolean - get() = message.isEmpty() && preview is ComposePreview.NoPreview + get() = message.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem companion object { fun saver(): Saver<MutableState<ComposeState>, *> = Saver( @@ -141,6 +126,8 @@ data class ComposeState( } } +private val maxFileSize = getMaxFileSize(FileProtocol.XFTP) + sealed class RecordingState { object NotStarted: RecordingState() class Started(val filePath: String, val progressMs: Int = 0): RecordingState() @@ -164,6 +151,74 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview { } } +@Composable +expect fun AttachmentSelection( + composeState: MutableState<ComposeState>, + attachmentOption: MutableState<AttachmentOption?>, + processPickedFile: (URI?, String?) -> Unit, + processPickedMedia: (List<URI>, String?) -> Unit +) + +fun MutableState<ComposeState>.processPickedFile(uri: URI?, text: String?) { + if (uri != null) { + val fileSize = getFileSize(uri) + if (fileSize != null && fileSize <= maxFileSize) { + val fileName = getFileName(uri) + if (fileName != null) { + value = value.copy(message = text ?: value.message, preview = ComposePreview.FilePreview(fileName, uri)) + } + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.large_file), + String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) + ) + } + } +} + +suspend fun MutableState<ComposeState>.processPickedMedia(uris: List<URI>, text: String?) { + val content = ArrayList<UploadContent>() + val imagesPreview = ArrayList<String>() + uris.forEach { uri -> + var bitmap: ImageBitmap? + when { + isImage(uri) -> { + // Image + val drawable = getDrawableFromUri(uri) + bitmap = getBitmapFromUri(uri) + if (isAnimImage(uri, drawable)) { + // It's a gif or webp + val fileSize = getFileSize(uri) + if (fileSize != null && fileSize <= maxFileSize) { + content.add(UploadContent.AnimatedImage(uri)) + } else { + bitmap = null + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.large_file), + String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) + ) + } + } else { + content.add(UploadContent.SimpleImage(uri)) + } + } + else -> { + // Video + val res = getBitmapFromVideo(uri) + bitmap = res.preview + val durationMs = res.duration + content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)) + } + } + if (bitmap != null) { + imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) + } + } + if (imagesPreview.isNotEmpty()) { + value = value.copy(message = text ?: value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) + } +} + @Composable fun ComposeView( chatModel: ChatModel, @@ -172,143 +227,23 @@ fun ComposeView( attachmentOption: MutableState<AttachmentOption?>, showChooseAttachment: () -> Unit ) { - val context = LocalContext.current val linkUrl = rememberSaveable { mutableStateOf<String?>(null) } val prevLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } val pendingLinkUrl = rememberSaveable { mutableStateOf<String?>(null) } val cancelledLinks = rememberSaveable { mutableSetOf<String>() } val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val maxFileSize = getMaxFileSize(FileProtocol.XFTP) + val saveLastDraft = chatModel.controller.appPrefs.privacySaveLastDraft.get() val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) - val textStyle = remember { mutableStateOf(smallFont) } - val cameraLauncher = rememberCameraLauncher { uri: Uri? -> - if (uri != null) { - val bitmap: Bitmap? = getBitmapFromUri(uri) - if (bitmap != null) { - val imagePreview = resizeImageToStrSize(bitmap, maxDataSize = 14000) - composeState.value = composeState.value.copy(preview = ComposePreview.MediaPreview(listOf(imagePreview), listOf(UploadContent.SimpleImage(uri)))) - } - } - } - val cameraPermissionLauncher = rememberPermissionLauncher { isGranted: Boolean -> - if (isGranted) { - cameraLauncher.launchWithFallback() - } else { - Toast.makeText(context, generalGetString(MR.strings.toast_permission_denied), Toast.LENGTH_SHORT).show() - } - } - val processPickedMedia = { uris: List<Uri>, text: String? -> - val content = ArrayList<UploadContent>() - val imagesPreview = ArrayList<String>() - uris.forEach { uri -> - var bitmap: Bitmap? = null - val isImage = MimeTypeMap.getSingleton().getMimeTypeFromExtension(getFileName(uri)?.split(".")?.last())?.contains("image/") == true - when { - isImage -> { - // Image - val drawable = getDrawableFromUri(uri) - bitmap = if (drawable != null) getBitmapFromUri(uri) else null - val isAnimNewApi = Build.VERSION.SDK_INT >= 28 && drawable is AnimatedImageDrawable - val isAnimOldApi = Build.VERSION.SDK_INT < 28 && - (getFileName(uri)?.endsWith(".gif") == true || getFileName(uri)?.endsWith(".webp") == true) - if (isAnimNewApi || isAnimOldApi) { - // It's a gif or webp - val fileSize = getFileSize(uri) - if (fileSize != null && fileSize <= maxFileSize) { - content.add(UploadContent.AnimatedImage(uri)) - } else { - bitmap = null - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) - ) - } - } else { - content.add(UploadContent.SimpleImage(uri)) - } - } - else -> { - // Video - val res = getBitmapFromVideo(uri) - bitmap = res.preview - val durationMs = res.duration - content.add(UploadContent.Video(uri, durationMs?.div(1000)?.toInt() ?: 0)) - } - } - if (bitmap != null) { - imagesPreview.add(resizeImageToStrSize(bitmap, maxDataSize = 14000)) - } - } - if (imagesPreview.isNotEmpty()) { - composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.MediaPreview(imagesPreview, content)) - } - } - val processPickedFile = { uri: Uri?, text: String? -> - if (uri != null) { - val fileSize = getFileSize(uri) - if (fileSize != null && fileSize <= maxFileSize) { - val fileName = getFileName(uri) - if (fileName != null) { - composeState.value = composeState.value.copy(message = text ?: composeState.value.message, preview = ComposePreview.FilePreview(fileName, uri)) - } - } else { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.large_file), - String.format(generalGetString(MR.strings.maximum_supported_file_size), formatBytes(maxFileSize)) - ) - } - } - } - val galleryImageLauncher = rememberLauncherForActivityResult(contract = PickMultipleImagesFromGallery()) { processPickedMedia(it, null) } - val galleryImageLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) } - val galleryVideoLauncher = rememberLauncherForActivityResult(contract = PickMultipleVideosFromGallery()) { processPickedMedia(it, null) } - val galleryVideoLauncherFallback = rememberGetMultipleContentsLauncher { processPickedMedia(it, null) } - - val filesLauncher = rememberGetContentLauncher { processPickedFile(it, null) } + val textStyle = remember(MaterialTheme.colors.isLight) { mutableStateOf(smallFont) } val recState: MutableState<RecordingState> = remember { mutableStateOf(RecordingState.NotStarted) } - LaunchedEffect(attachmentOption.value) { - when (attachmentOption.value) { - AttachmentOption.CameraPhoto -> { - when (PackageManager.PERMISSION_GRANTED) { - ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) -> { - cameraLauncher.launchWithFallback() - } - else -> { - cameraPermissionLauncher.launch(Manifest.permission.CAMERA) - } - } - attachmentOption.value = null - } - AttachmentOption.GalleryImage -> { - try { - galleryImageLauncher.launch(0) - } catch (e: ActivityNotFoundException) { - galleryImageLauncherFallback.launch("image/*") - } - attachmentOption.value = null - } - AttachmentOption.GalleryVideo -> { - try { - galleryVideoLauncher.launch(0) - } catch (e: ActivityNotFoundException) { - galleryVideoLauncherFallback.launch("video/*") - } - attachmentOption.value = null - } - AttachmentOption.File -> { - filesLauncher.launch("*/*") - attachmentOption.value = null - } - else -> {} - } - } + AttachmentSelection(composeState, attachmentOption, composeState::processPickedFile) { uris, text -> CoroutineScope(Dispatchers.IO).launch { composeState.processPickedMedia(uris, text) } } fun isSimplexLink(link: String): Boolean = link.startsWith("https://simplex.chat", true) || link.startsWith("http://simplex.chat", true) fun parseMessage(msg: String): String? { - val parsedMsg = runBlocking { chatModel.controller.apiParseMarkdown(msg) } + val parsedMsg = parseToMarkdown(msg) val link = parsedMsg?.firstOrNull { ft -> ft.format is Format.Uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text) } return link?.text } @@ -351,6 +286,20 @@ fun ComposeView( cancelledLinks.clear() } + fun clearPrevDraft(prevChatId: String?) { + if (chatModel.draftChatId.value == prevChatId) { + chatModel.draft.value = null + chatModel.draftChatId.value = null + } + } + + fun clearCurrentDraft() { + if (chatModel.draftChatId.value == chat.id) { + chatModel.draft.value = null + chatModel.draftChatId.value = null + } + } + fun clearState(live: Boolean = false) { if (live) { composeState.value = composeState.value.copy(inProgress = false) @@ -368,7 +317,7 @@ fun ComposeView( chatModel.filesToDelete.clear() } - suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: String? = null, live: Boolean = false, ttl: Int?): ChatItem? { + suspend fun send(cInfo: ChatInfo, mc: MsgContent, quoted: Long?, file: CryptoFile? = null, live: Boolean = false, ttl: Int?): ChatItem? { val aChatItem = chatModel.controller.apiSendMessage( type = cInfo.chatType, id = cInfo.apiId, @@ -382,12 +331,10 @@ fun ComposeView( chatModel.addChatItem(cInfo, aChatItem.chatItem) return aChatItem.chatItem } - if (file != null) removeFile(file) + if (file != null) removeFile(file.filePath) return null } - - suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? { val cInfo = chat.chatInfo val cs = composeState.value @@ -409,6 +356,7 @@ fun ComposeView( MsgContent.MCText(msgText) } } + else -> MsgContent.MCText(msgText) } } @@ -425,6 +373,14 @@ fun ComposeView( } } + suspend fun sendMemberContactInvitation() { + val mc = checkLinkPreview() + val contact = chatModel.controller.apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) + if (contact != null) { + chatModel.updateContact(contact) + } + } + suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? { val oldMsgContent = ei.content.msgContent if (oldMsgContent != null) { @@ -446,24 +402,28 @@ fun ComposeView( if (liveMessage != null) composeState.value = cs.copy(liveMessage = null) sending() } + clearCurrentDraft() - if (cs.contextItem is ComposeContextItem.EditingItem) { + if (chat.nextSendGrpInv) { + sendMemberContactInvitation() + sent = null + } else if (cs.contextItem is ComposeContextItem.EditingItem) { val ei = cs.contextItem.chatItem sent = updateMessage(ei, cInfo, live) } else if (liveMessage != null && liveMessage.sent) { sent = updateMessage(liveMessage.chatItem, cInfo, live) } else { val msgs: ArrayList<MsgContent> = ArrayList() - val files: ArrayList<String> = ArrayList() + val files: ArrayList<CryptoFile> = ArrayList() when (val preview = cs.preview) { ComposePreview.NoPreview -> msgs.add(MsgContent.MCText(msgText)) is ComposePreview.CLinkPreview -> msgs.add(checkLinkPreview()) is ComposePreview.MediaPreview -> { preview.content.forEachIndexed { index, it -> val file = when (it) { - is UploadContent.SimpleImage -> saveImage(it.uri) - is UploadContent.AnimatedImage -> saveAnimImage(it.uri) - is UploadContent.Video -> saveFileFromUri(it.uri) + is UploadContent.SimpleImage -> saveImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) + is UploadContent.AnimatedImage -> saveAnimImage(it.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) + is UploadContent.Video -> saveFileFromUri(it.uri, encrypted = false) } if (file != null) { files.add(file) @@ -478,16 +438,22 @@ fun ComposeView( is ComposePreview.VoicePreview -> { val tmpFile = File(preview.voice) AudioPlayer.stop(tmpFile.absolutePath) - val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderNative.extension, ""))) - withContext(Dispatchers.IO) { - Files.move(tmpFile.toPath(), actualFile.toPath()) - } - files.add(actualFile.name) + val actualFile = File(getAppFilePath(tmpFile.name.replaceAfter(RecorderInterface.extension, ""))) + files.add(withContext(Dispatchers.IO) { + if (chatController.appPrefs.privacyEncryptLocalFiles.get()) { + val args = encryptCryptoFile(tmpFile.absolutePath, actualFile.absolutePath) + tmpFile.delete() + CryptoFile(actualFile.name, args) + } else { + Files.move(tmpFile.toPath(), actualFile.toPath()) + CryptoFile.plain(actualFile.name) + } + }) deleteUnusedFiles() msgs.add(MsgContent.MCVoice(if (msgs.isEmpty()) msgText else "", preview.durationMs / 1000)) } is ComposePreview.FilePreview -> { - val file = saveFileFromUri(preview.uri) + val file = saveFileFromUri(preview.uri, encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get()) if (file != null) { files.add((file)) msgs.add(MsgContent.MCFile(if (msgs.isEmpty()) msgText else "")) @@ -510,7 +476,7 @@ fun ComposeView( (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview || cs.preview is ComposePreview.VoicePreview) - ) { + ) { sent = send(cInfo, MsgContent.MCText(msgText), quotedItemId, null, live, ttl) } } @@ -568,7 +534,7 @@ fun ComposeView( recState.value = RecordingState.NotStarted composeState.value = composeState.value.copy(preview = ComposePreview.NoPreview) withBGApi { - RecorderNative.stopRecording?.invoke() + RecorderInterface.stopRecording?.invoke() AudioPlayer.stop(filePath) filePath?.let { File(it).delete() } } @@ -627,6 +593,14 @@ fun ComposeView( } } + fun editPrevMessage() { + if (composeState.value.contextItem != ComposeContextItem.NoContextItem || composeState.value.preview != ComposePreview.NoPreview) return + val lastEditable = chatModel.chatItems.findLast { it.meta.editable } + if (lastEditable != null) { + composeState.value = ComposeState(editingItem = lastEditable, useLinkPreviews = useLinkPreviews) + } + } + @Composable fun previewView() { when (val preview = composeState.value.preview) { @@ -683,17 +657,22 @@ fun ComposeView( when (val shared = chatModel.sharedContent.value) { is SharedContent.Text -> onMessageChange(shared.text) - is SharedContent.Media -> processPickedMedia(shared.uris, shared.text) - is SharedContent.File -> processPickedFile(shared.uri, shared.text) + is SharedContent.Media -> composeState.processPickedMedia(shared.uris, shared.text) + is SharedContent.File -> composeState.processPickedFile(shared.uri, shared.text) null -> {} } chatModel.sharedContent.value = null } val userCanSend = rememberUpdatedState(chat.userCanSend) + val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) val userIsObserver = rememberUpdatedState(chat.userIsObserver) + val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) Column { + if (nextSendGrpInv.value) { + ComposeContextInvitingContactMemberView() + } if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) { contextItemView() when { @@ -726,11 +705,21 @@ fun ComposeView( } else { showChooseAttachment } - IconButton(attachmentClicked, enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value) { + val attachmentEnabled = + !composeState.value.attachmentDisabled + && sendMsgEnabled.value + && userCanSend.value + && !isGroupAndProhibitedFiles + && !nextSendGrpInv.value + IconButton( + attachmentClicked, + Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp), + enabled = attachmentEnabled + ) { Icon( painterResource(MR.images.ic_attach_file_filled_500), contentDescription = stringResource(MR.strings.attach), - tint = if (!composeState.value.attachmentDisabled && userCanSend.value && !isGroupAndProhibitedFiles) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier .size(28.dp) .clip(CircleShape) @@ -761,13 +750,6 @@ fun ComposeView( } } - fun clearCurrentDraft() { - if (chatModel.draftChatId.value == chat.id) { - chatModel.draft.value = null - chatModel.draftChatId.value = null - } - } - LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) { if (!chat.userCanSend) { clearCurrentDraft() @@ -775,46 +757,52 @@ fun ComposeView( } } - val activity = LocalContext.current as Activity - DisposableEffect(Unit) { - val orientation = activity.resources.configuration.orientation - onDispose { - if (orientation == activity.resources.configuration.orientation) { - val cs = composeState.value - if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { - sendMessage(null) - resetLinkPreview() - clearCurrentDraft() - deleteUnusedFiles() - } else if (composeState.value.inProgress) { - clearCurrentDraft() - } else if (!composeState.value.empty) { - if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { - composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) - } - chatModel.draft.value = composeState.value - chatModel.draftChatId.value = chat.id - } else { - clearCurrentDraft() - deleteUnusedFiles() - } - chatModel.removeLiveDummy() + KeyChangeEffect(chatModel.chatId.value) { prevChatId -> + val cs = composeState.value + if (cs.liveMessage != null && (cs.message.isNotEmpty() || cs.liveMessage.sent)) { + sendMessage(null) + resetLinkPreview() + clearPrevDraft(prevChatId) + deleteUnusedFiles() + } else if (cs.inProgress) { + clearPrevDraft(prevChatId) + } else if (!cs.empty) { + if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { + composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) } + if (saveLastDraft) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = prevChatId + } + composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) + } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { + composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) + } else { + clearPrevDraft(prevChatId) + deleteUnusedFiles() } + chatModel.removeLiveDummy() } val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } + val sendButtonColor = + if (chat.chatInfo.incognito) + if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) + else MaterialTheme.colors.primary SendMsgView( composeState, showVoiceRecordIcon = true, recState, chat.chatInfo is ChatInfo.Direct, liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown, + sendMsgEnabled = sendMsgEnabled.value, + nextSendGrpInv = nextSendGrpInv.value, needToAllowVoiceToContact, allowedVoiceByPrefs, allowVoiceToContact = ::allowVoiceToContact, userIsObserver = userIsObserver.value, userCanSend = userCanSend.value, + sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, sendMessage = { ttl -> @@ -827,71 +815,10 @@ fun ComposeView( composeState.value = composeState.value.copy(liveMessage = null) chatModel.removeLiveDummy() }, + editPrevMessage = ::editPrevMessage, onMessageChange = ::onMessageChange, textStyle = textStyle ) } } } - -class PickFromGallery: ActivityResultContract<Int, Uri?>() { - override fun createIntent(context: Context, input: Int) = - Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { - type = "image/*" - } - - override fun parseResult(resultCode: Int, intent: Intent?): Uri? = intent?.data -} - -class PickMultipleImagesFromGallery: ActivityResultContract<Int, List<Uri>>() { - override fun createIntent(context: Context, input: Int) = - Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI).apply { - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - type = "image/*" - } - - override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> = - if (intent?.data != null) - listOf(intent.data!!) - else if (intent?.clipData != null) - with(intent.clipData!!) { - val uris = ArrayList<Uri>() - for (i in 0 until kotlin.math.min(itemCount, 10)) { - val uri = getItemAt(i).uri - if (uri != null) uris.add(uri) - } - if (itemCount > 10) { - AlertManager.shared.showAlertMsg(MR.strings.images_limit_title, MR.strings.images_limit_desc) - } - uris - } - else - emptyList() -} - - -class PickMultipleVideosFromGallery: ActivityResultContract<Int, List<Uri>>() { - override fun createIntent(context: Context, input: Int) = - Intent(Intent.ACTION_PICK, MediaStore.Video.Media.INTERNAL_CONTENT_URI).apply { - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - type = "video/*" - } - - override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> = - if (intent?.data != null) - listOf(intent.data!!) - else if (intent?.clipData != null) - with(intent.clipData!!) { - val uris = ArrayList<Uri>() - for (i in 0 until kotlin.math.min(itemCount, 10)) { - val uri = getItemAt(i).uri - if (uri != null) uris.add(uri) - } - if (itemCount > 10) { - AlertManager.shared.showAlertMsg(MR.strings.videos_limit_title, MR.strings.videos_limit_desc) - } - uris - } - else - emptyList() -} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt similarity index 93% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt index 6ea0b345f8..a4c90d30dd 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ComposeVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt @@ -1,4 +1,7 @@ +package chat.simplex.common.views.chat + import androidx.compose.animation.core.Animatable +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -12,13 +15,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.durationText -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.CryptoFile +import chat.simplex.common.model.durationText +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.platform.AudioPlayer import chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged @@ -50,7 +53,7 @@ fun ComposeVoiceView( IconButton( onClick = { if (!audioPlaying.value) { - AudioPlayer.play(filePath, audioPlaying, progress, duration, false) + AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, false) } else { AudioPlayer.pause(audioPlaying, progress) } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 046571e008..465603d409 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat import InfoRow import SectionBottomSpacer @@ -15,11 +15,10 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.PreferenceToggle +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.PreferenceToggle +import chat.simplex.common.model.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt similarity index 67% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index a92db078c3..49203c7cfb 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -10,12 +10,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.* +import androidx.compose.ui.unit.sp +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.* +import chat.simplex.common.model.* import chat.simplex.res.MR import kotlinx.datetime.Clock @@ -28,6 +29,17 @@ fun ContextItemView( val sent = contextItem.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + + @Composable + fun msgContentView(lines: Int) { + MarkdownText( + contextItem.text, contextItem.formattedText, + maxLines = lines, + linkMode = SimplexLinkMode.DESCRIPTION, + modifier = Modifier.fillMaxWidth(), + ) + } + Row( Modifier .padding(top = 8.dp) @@ -50,12 +62,21 @@ fun ContextItemView( contentDescription = stringResource(MR.strings.icon_descr_context), tint = MaterialTheme.colors.secondary, ) - MarkdownText( - contextItem.text, contextItem.formattedText, - sender = contextItem.memberDisplayName, senderBold = true, maxLines = 3, - linkMode = SimplexLinkMode.DESCRIPTION, - modifier = Modifier.fillMaxWidth(), - ) + val sender = contextItem.memberDisplayName + if (sender != null) { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + msgContentView(lines = 2) + } + } else { + msgContentView(lines = 3) + } } IconButton(onClick = cancelContextItem) { Icon( diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ScanCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt similarity index 54% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ScanCodeView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt index 69304f8398..91fb4a6e8b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/ScanCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.kt @@ -1,30 +1,20 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat -import android.Manifest import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCodeScanner -import com.google.accompanist.permissions.rememberPermissionState +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCodeScanner import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource @Composable -fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { - val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) - LaunchedEffect(Unit) { - cameraPermissionState.launchPermissionRequest() - } - ScanCodeLayout(verifyCode, close) -} +expect fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) @Composable -private fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { +fun ScanCodeLayout(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { Column( Modifier .fillMaxSize() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt similarity index 71% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 4c2bf1ca27..2d696b7781 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -1,20 +1,7 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.content.pm.ActivityInfo -import android.content.res.Configuration -import android.os.Build -import android.text.InputType -import android.util.Log -import android.view.ViewGroup -import android.view.WindowManager -import android.view.inputmethod.* -import android.widget.EditText -import android.widget.TextView import androidx.compose.animation.core.* +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -28,32 +15,20 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.* -import androidx.compose.ui.res.* import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.* -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* -import androidx.compose.ui.viewinterop.AndroidView -import androidx.compose.ui.window.Dialog -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.view.inputmethod.EditorInfoCompat -import androidx.core.view.inputmethod.InputConnectionCompat -import androidx.core.widget.* -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.ItemAction -import chat.simplex.app.views.helpers.* -import com.google.accompanist.permissions.rememberMultiplePermissionsState +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatItem +import chat.simplex.common.platform.* +import chat.simplex.common.views.usersettings.showInDevelopingAlert import chat.simplex.res.MR -import dev.icerock.moko.resources.StringResource -import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource +import dev.icerock.moko.resources.compose.painterResource import kotlinx.coroutines.* -import java.lang.reflect.Field @Composable fun SendMsgView( @@ -62,10 +37,13 @@ fun SendMsgView( recState: MutableState<RecordingState>, isDirectChat: Boolean, liveMessageAlertShown: SharedPreference<Boolean>, + sendMsgEnabled: Boolean, + nextSendGrpInv: Boolean, needToAllowVoiceToContact: Boolean, allowedVoiceByPrefs: Boolean, userIsObserver: Boolean, userCanSend: Boolean, + sendButtonColor: Color = MaterialTheme.colors.primary, allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, customDisappearingMessageTimePref: SharedPreference<Int>? = null, @@ -73,6 +51,7 @@ fun SendMsgView( sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, cancelLiveMessage: (() -> Unit)? = null, + editPrevMessage: () -> Unit, onMessageChange: (String) -> Unit, textStyle: MutableState<TextStyle> ) { @@ -88,13 +67,25 @@ fun SendMsgView( Box(Modifier.padding(vertical = 8.dp)) { val cs = composeState.value - val showProgress = cs.inProgress && (cs.preview is ComposePreview.MediaPreview || cs.preview is ComposePreview.FilePreview) - val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && + var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(composeState.value.inProgress) { + progressByTimeout = if (composeState.value.inProgress) { + delay(500) + composeState.value.inProgress + } else { + false + } + } + val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } - NativeKeyboard(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange) + PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) { + if (!cs.inProgress) { + sendMessage(null) + } + } // Disable clicks on text field - if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) { + if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) { Box( Modifier .matchParentSize() @@ -109,10 +100,9 @@ fun SendMsgView( if (showDeleteTextButton.value) { DeleteTextButton(composeState) } - Box(Modifier.align(Alignment.BottomEnd)) { + Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) { val sendButtonSize = remember { Animatable(36f) } val sendButtonAlpha = remember { Animatable(1f) } - val permissionsState = rememberMultiplePermissionsState(listOf(Manifest.permission.RECORD_AUDIO)) val scope = rememberCoroutineScope() LaunchedEffect(Unit) { // Making LiveMessage alive when screen orientation was changed @@ -121,8 +111,8 @@ fun SendMsgView( } } when { - showProgress -> ProgressIndicator() - showVoiceButton -> { + progressByTimeout -> ProgressIndicator() + showVoiceButton && sendMsgEnabled -> { Row(verticalAlignment = Alignment.CenterVertically) { val stopRecOnNextClick = remember { mutableStateOf(false) } when { @@ -135,8 +125,8 @@ fun SendMsgView( } } } - !permissionsState.allPermissionsGranted -> - VoiceButtonWithoutPermission { permissionsState.launchMultiplePermissionRequest() } + !allowedToRecordVoiceByPlatform() -> + VoiceButtonWithoutPermissionByPlatform() else -> RecordVoiceView(recState, stopRecOnNextClick) } @@ -162,7 +152,7 @@ fun SendMsgView( else -> { val cs = composeState.value val icon = if (cs.editing || cs.liveMessage != null) painterResource(MR.images.ic_check_filled) else painterResource(MR.images.ic_arrow_upward) - val disabled = !cs.sendEnabled() || + val disabled = !sendMsgEnabled || !cs.sendEnabled() || (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) || cs.endLiveDisabled val showDropdown = rememberSaveable { mutableStateOf(false) } @@ -171,7 +161,7 @@ fun SendMsgView( fun MenuItems(): List<@Composable () -> Unit> { val menuItems = mutableListOf<@Composable () -> Unit>() - if (cs.liveMessage == null && !cs.editing) { + if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) { if ( cs.preview !is ComposePreview.VoicePreview && cs.contextItem is ComposeContextItem.NoContextItem && @@ -207,12 +197,12 @@ fun SendMsgView( val menuItems = MenuItems() if (menuItems.isNotEmpty()) { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) { showDropdown.value = true } + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) { showDropdown.value = true } DefaultDropdownMenu(showDropdown) { menuItems.forEach { composable -> composable() } } } else { - SendMsgButton(icon, sendButtonSize, sendButtonAlpha, !disabled, sendMessage) + SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !disabled, sendMessage) } } } @@ -220,6 +210,12 @@ fun SendMsgView( } } +@Composable +expect fun allowedToRecordVoiceByPlatform(): Boolean + +@Composable +expect fun VoiceButtonWithoutPermissionByPlatform() + @Composable private fun CustomDisappearingMessageDialog( sendMessage: (Int?) -> Unit, @@ -258,7 +254,7 @@ private fun CustomDisappearingMessageDialog( } } - Dialog(onDismissRequest = { setShowDialog(false) }) { + DefaultDialog(onDismissRequest = { setShowDialog(false) }) { Surface( shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) { @@ -290,7 +286,6 @@ private fun CustomDisappearingMessageDialog( .clickable { setShowDialog(false) } ) } - ChoiceButton(generalGetString(MR.strings.send_disappearing_message_30_seconds)) { sendMessage(30) setShowDialog(false) @@ -313,124 +308,6 @@ private fun CustomDisappearingMessageDialog( } } -@Composable -private fun NativeKeyboard( - composeState: MutableState<ComposeState>, - textStyle: MutableState<TextStyle>, - showDeleteTextButton: MutableState<Boolean>, - userIsObserver: Boolean, - onMessageChange: (String) -> Unit -) { - val cs = composeState.value - val textColor = MaterialTheme.colors.onBackground - val tintColor = MaterialTheme.colors.secondaryVariant - val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) - val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } - val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } - val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() } - val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() } - var showKeyboard by remember { mutableStateOf(false) } - LaunchedEffect(cs.contextItem) { - if (cs.contextItem is ComposeContextItem.QuotedItem) { - delay(100) - showKeyboard = true - } else if (cs.contextItem is ComposeContextItem.EditingItem) { - // Keyboard will not show up if we try to show it too fast - delay(300) - showKeyboard = true - } - } - - AndroidView(modifier = Modifier, factory = { - val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { - override fun setOnReceiveContentListener( - mimeTypes: Array<out String>?, - listener: android.view.OnReceiveContentListener? - ) { - super.setOnReceiveContentListener(mimeTypes, listener) - } - - override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { - val connection = super.onCreateInputConnection(editorInfo) - EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) - val onCommit = InputConnectionCompat.OnCommitContentListener { inputContentInfo, _, _ -> - try { - inputContentInfo.requestPermission() - } catch (e: Exception) { - return@OnCommitContentListener false - } - SimplexApp.context.chatModel.sharedContent.value = SharedContent.Media("", listOf(inputContentInfo.contentUri)) - true - } - return InputConnectionCompat.createWrapper(connection, editorInfo, onCommit) - } - } - editText.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - editText.maxLines = 16 - editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType - editText.setTextColor(textColor.toArgb()) - editText.textSize = textStyle.value.fontSize.value - val drawable = it.getDrawable(R.drawable.send_msg_view_background)!! - DrawableCompat.setTint(drawable, tintColor.toArgb()) - editText.background = drawable - editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) - editText.setText(cs.message) - if (Build.VERSION.SDK_INT >= 29) { - editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) } - } else { - try { - val f: Field = TextView::class.java.getDeclaredField("mCursorDrawableRes") - f.isAccessible = true - f.set(editText, R.drawable.edit_text_cursor) - } catch (e: Exception) { - Log.e(chat.simplex.app.TAG, e.stackTraceToString()) - } - } - editText.doOnTextChanged { text, _, _, _ -> - if (!composeState.value.inProgress) { - onMessageChange(text.toString()) - } else if (text.toString() != composeState.value.message) { - editText.setText(composeState.value.message) - } - } - editText.doAfterTextChanged { text -> if (composeState.value.preview is ComposePreview.VoicePreview && text.toString() != "") editText.setText("") } - editText - }) { - it.setTextColor(textColor.toArgb()) - it.textSize = textStyle.value.fontSize.value - DrawableCompat.setTint(it.background, tintColor.toArgb()) - it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview - it.isFocusableInTouchMode = it.isFocusable - if (cs.message != it.text.toString()) { - it.setText(cs.message) - // Set cursor to the end of the text - it.setSelection(it.text.length) - } - if (showKeyboard) { - it.requestFocus() - val imm: InputMethodManager = SimplexApp.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) - showKeyboard = false - } - showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress - } - if (composeState.value.preview is ComposePreview.VoicePreview) { - ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) - } else if (userIsObserver) { - ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) - } -} - -@Composable -private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) { - Text( - generalGetString(textId), - Modifier.padding(padding), - color = MaterialTheme.colors.secondary, - style = textStyle.value.copy(fontStyle = FontStyle.Italic) - ) -} - @Composable private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) { IconButton( @@ -443,7 +320,7 @@ private fun BoxScope.DeleteTextButton(composeState: MutableState<ComposeState>) @Composable private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNextClick: MutableState<Boolean>) { - val rec: Recorder = remember { RecorderNative() } + val rec: RecorderInterface = remember { RecorderNative() } DisposableEffect(Unit) { onDispose { rec.stop() } } val stopRecordingAndAddAudio: () -> Unit = { recState.value.filePathNullable?.let { @@ -460,7 +337,10 @@ private fun RecordVoiceView(recState: MutableState<RecordingState>, stopRecOnNex LockToCurrentOrientationUntilDispose() StopRecordButton(stopRecordingAndAddAudio) } else { - val startRecording: () -> Unit = { + val startRecording: () -> Unit = out@ { + if (appPlatform.isDesktop) { + return@out showInDevelopingAlert() + } recState.value = RecordingState.Started( filePath = rec.start { progress: Int?, finished: Boolean -> val state = recState.value @@ -505,7 +385,7 @@ private fun DisallowedVoiceButton(enabled: Boolean, onClick: () -> Unit) { } @Composable -private fun VoiceButtonWithoutPermission(onClick: () -> Unit) { +fun VoiceButtonWithoutPermission(onClick: () -> Unit) { IconButton(onClick, Modifier.size(36.dp)) { Icon( painterResource(MR.images.ic_keyboard_voice_filled), @@ -518,24 +398,6 @@ private fun VoiceButtonWithoutPermission(onClick: () -> Unit) { } } -@Composable -private fun LockToCurrentOrientationUntilDispose() { - val context = LocalContext.current - DisposableEffect(Unit) { - val activity = context as Activity - val manager = context.getSystemService(Activity.WINDOW_SERVICE) as WindowManager - val rotation = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) manager.defaultDisplay.rotation else activity.display?.rotation - activity.requestedOrientation = when (rotation) { - android.view.Surface.ROTATION_90 -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE - android.view.Surface.ROTATION_180 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT - android.view.Surface.ROTATION_270 -> ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE - else -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT - } - // Unlock orientation - onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } - } -} - @Composable private fun StopRecordButton(onClick: () -> Unit) { IconButton(onClick, Modifier.size(36.dp)) { @@ -590,6 +452,7 @@ private fun SendMsgButton( icon: Painter, sizeDp: Animatable<Float, AnimationVector1D>, alpha: Animatable<Float, AnimationVector1D>, + sendButtonColor: Color, enabled: Boolean, sendMessage: (Int?) -> Unit, onLongClick: (() -> Unit)? = null @@ -604,7 +467,8 @@ private fun SendMsgButton( role = Role.Button, interactionSource = interactionSource, indication = rememberRipple(bounded = false, radius = 24.dp) - ), + ) + .onRightClick { onLongClick?.invoke() }, contentAlignment = Alignment.Center ) { Icon( @@ -616,7 +480,7 @@ private fun SendMsgButton( .padding(4.dp) .alpha(alpha.value) .clip(CircleShape) - .background(if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) + .background(if (enabled) sendButtonColor else MaterialTheme.colors.secondary) .padding(3.dp) ) } @@ -721,12 +585,11 @@ private fun showDisabledVoiceAlert(isDirectChat: Boolean) { ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewSendMsgView() { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) @@ -738,6 +601,8 @@ fun PreviewSendMsgView() { recState = remember { mutableStateOf(RecordingState.NotStarted) }, isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), + sendMsgEnabled = true, + nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, userIsObserver = false, @@ -745,18 +610,18 @@ fun PreviewSendMsgView() { allowVoiceToContact = {}, timedMessageAllowed = false, sendMessage = {}, + editPrevMessage = {}, onMessageChange = { _ -> }, textStyle = textStyle ) } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewSendMsgViewEditing() { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) @@ -769,6 +634,8 @@ fun PreviewSendMsgViewEditing() { recState = remember { mutableStateOf(RecordingState.NotStarted) }, isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), + sendMsgEnabled = true, + nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, userIsObserver = false, @@ -776,18 +643,18 @@ fun PreviewSendMsgViewEditing() { allowVoiceToContact = {}, timedMessageAllowed = false, sendMessage = {}, + editPrevMessage = {}, onMessageChange = { _ -> }, textStyle = textStyle ) } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewSendMsgViewInProgress() { val smallFont = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground) @@ -800,6 +667,8 @@ fun PreviewSendMsgViewInProgress() { recState = remember { mutableStateOf(RecordingState.NotStarted) }, isDirectChat = true, liveMessageAlertShown = SharedPreference(get = { true }, set = { }), + sendMsgEnabled = true, + nextSendGrpInv = false, needToAllowVoiceToContact = false, allowedVoiceByPrefs = true, userIsObserver = false, @@ -807,6 +676,7 @@ fun PreviewSendMsgViewInProgress() { allowVoiceToContact = {}, timedMessageAllowed = false, sendMessage = {}, + editPrevMessage = {}, onMessageChange = { _ -> }, textStyle = textStyle ) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt similarity index 84% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt index 9a122f360b..1b25c52cfe 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/VerifyCodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/VerifyCodeView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat +package chat.simplex.common.views.chat import SectionBottomSpacer import SectionView @@ -10,15 +10,17 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCode +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.shareText +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode import chat.simplex.res.MR @Composable @@ -87,7 +89,8 @@ private fun VerifyCodeLayout( ) } Box(Modifier.weight(1f)) { - IconButton({ shareText(connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) { + val clipboard = LocalClipboardManager.current + IconButton({ clipboard.shareText(connectionCode) }, Modifier.size(20.dp).align(Alignment.CenterStart)) { Icon(painterResource(MR.images.ic_share_filled), null, tint = MaterialTheme.colors.primary) } } @@ -108,9 +111,11 @@ private fun VerifyCodeLayout( verifyCode(null) {} } } else { - SimpleButton(generalGetString(MR.strings.scan_code), painterResource(MR.images.ic_qr_code)) { - ModalManager.shared.showModal { - ScanCodeView(verifyCode) { } + if (appPlatform.isAndroid) { + SimpleButton(generalGetString(MR.strings.scan_code), painterResource(MR.images.ic_qr_code)) { + ModalManager.end.showModal { + ScanCodeView(verifyCode) { } + } } } SimpleButton(generalGetString(MR.strings.mark_code_verified), painterResource(MR.images.ic_verified_user)) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt similarity index 90% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index d061f15885..90ab1b45ff 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import SectionBottomSpacer import SectionCustomFooter @@ -6,7 +6,6 @@ import SectionDividerSpaced import SectionItemView import SectionSpacer import SectionView -import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -16,20 +15,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalView import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.ChatInfoToolbarTitle -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.InfoAboutIncognito -import chat.simplex.app.views.usersettings.SettingsActionItem +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ChatInfoToolbarTitle +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.* import chat.simplex.res.MR @Composable @@ -40,7 +40,6 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } BackHandler(onBack = close) AddGroupMembersLayout( - chatModel.incognito.value, groupInfo = groupInfo, creatingGroup = creatingGroup, contactsToAdd = getContactsToAdd(chatModel, searchText.value.text), @@ -49,7 +48,7 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch allowModifyMembers = allowModifyMembers, searchText, openPreferences = { - ModalManager.shared.showCustomModal { close -> + ModalManager.end.showCustomModal { close -> GroupPreferencesView(chatModel, groupInfo.id, close) } }, @@ -72,6 +71,9 @@ fun AddGroupMembersView(groupInfo: GroupInfo, creatingGroup: Boolean = false, ch removeContact = { contactId -> selectedContacts.removeIf { it == contactId } }, close = close, ) + KeyChangeEffect(chatModel.chatId.value) { + close() + } } fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> { @@ -91,7 +93,6 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> { @Composable fun AddGroupMembersLayout( - chatModelIncognito: Boolean, groupInfo: GroupInfo, creatingGroup: Boolean, contactsToAdd: List<Contact>, @@ -106,19 +107,31 @@ fun AddGroupMembersLayout( removeContact: (Long) -> Unit, close: () -> Unit, ) { + @Composable fun profileText() { + Row( + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + painterResource(MR.images.ic_info), + null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier.padding(end = 10.dp).size(20.dp) + ) + Text(generalGetString(MR.strings.group_main_profile_sent), textAlign = TextAlign.Center, style = MaterialTheme.typography.body2) + } + } + Column( Modifier .fillMaxWidth() .verticalScroll(rememberScrollState()), ) { AppBarTitle(stringResource(MR.strings.button_add_members)) - InfoAboutIncognito( - chatModelIncognito, - false, - generalGetString(MR.strings.group_unsupported_incognito_main_profile_sent), - generalGetString(MR.strings.group_main_profile_sent), - true - ) + profileText() Spacer(Modifier.size(DEFAULT_PADDING)) Row( Modifier.fillMaxWidth(), @@ -163,7 +176,7 @@ fun AddGroupMembersLayout( SectionDividerSpaced(maxTopPadding = true) SectionView(stringResource(MR.strings.select_contacts)) { SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) { - SearchRowView(searchText, selectedContacts.size) + SearchRowView(searchText) } ContactList(contacts = contactsToAdd, selectedContacts, groupInfo, allowModifyMembers, addContact, removeContact) } @@ -174,8 +187,7 @@ fun AddGroupMembersLayout( @Composable private fun SearchRowView( - searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }, - selectedContactsSize: Int + searchText: MutableState<TextFieldValue> = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } ) { Box(Modifier.width(36.dp), contentAlignment = Alignment.Center) { Icon(painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.secondary) @@ -184,11 +196,6 @@ private fun SearchRowView( SearchTextField(Modifier.fillMaxWidth(), searchText = searchText, alwaysVisible = true) { searchText.value = searchText.value.copy(it) } - val view = LocalView.current - LaunchedEffect(selectedContactsSize) { - searchText.value = searchText.value.copy("") - hideKeyboard(view) - } } @Composable @@ -349,7 +356,6 @@ fun showProhibitedToInviteIncognitoAlertDialog() { fun PreviewAddGroupMembersLayout() { SimpleXTheme { AddGroupMembersLayout( - chatModelIncognito = false, groupInfo = GroupInfo.sampleData, creatingGroup = false, contactsToAdd = listOf(Contact.sampleData, Contact.sampleData, Contact.sampleData), diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt similarity index 62% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index a368e2350f..f475d045cf 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer @@ -7,10 +7,9 @@ import SectionItemView import SectionSpacer import SectionTextFooter import SectionView -import android.util.Log -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.* +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -23,29 +22,42 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.TAG -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.* -import chat.simplex.app.views.chatlist.cantInviteIncognitoAlert -import chat.simplex.app.views.chatlist.setGroupMembers -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR +import kotlinx.coroutines.launch + +const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 @Composable -fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit) { BackHandler(onBack = close) val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } + val currentUser = chatModel.currentUser.value val developerTools = chatModel.controller.appPrefs.developerTools.get() - if (chat != null && chat.chatInfo is ChatInfo.Group) { + if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) { val groupInfo = chat.chatInfo.groupInfo + val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) } GroupChatInfoLayout( chat, groupInfo, + currentUser, + sendReceipts = sendReceipts, + setSendReceipts = { sendRcpts -> + withApi { + val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) + updateChatSettings(chat, chatSettings, chatModel) + sendReceipts.value = sendRcpts + } + }, members = chatModel.groupMembers .filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved } .sortedBy { it.displayName.lowercase() }, @@ -54,7 +66,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR addMembers = { withApi { setGroupMembers(groupInfo, chatModel) - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(groupInfo, false, chatModel, close) } } @@ -64,16 +76,12 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) val stats = r?.second val (_, code) = if (member.memberActive) { - try { - chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) - } catch (e: Exception) { - Log.e(TAG, e.stackTraceToString()) - member to null - } + val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) + member to memCode?.second } else { member to null } - ModalManager.shared.showModalCloseable(true) { closeCurrent -> + ModalManager.end.showModalCloseable(true) { closeCurrent -> remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem -> GroupMemberInfoView(groupInfo, mem, stats, code, chatModel, closeCurrent) { closeCurrent() @@ -84,13 +92,13 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR } }, editGroupProfile = { - ModalManager.shared.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) } + ModalManager.end.showCustomModal { close -> GroupProfileView(groupInfo, chatModel, close) } }, addOrEditWelcomeMessage = { - ModalManager.shared.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) } + ModalManager.end.showCustomModal { close -> GroupWelcomeView(chatModel, groupInfo, close) } }, openPreferences = { - ModalManager.shared.showCustomModal { close -> + ModalManager.end.showCustomModal { close -> GroupPreferencesView( chatModel, chat.id, @@ -102,7 +110,7 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR clearChat = { clearChatDialog(chat.chatInfo, chatModel, close) }, leaveGroup = { leaveGroupDialog(groupInfo, chatModel, close) }, manageGroupLink = { - ModalManager.shared.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } + ModalManager.end.showModal { GroupLinkView(chatModel, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } } ) } @@ -121,8 +129,11 @@ fun deleteGroupDialog(chatInfo: ChatInfo, groupInfo: GroupInfo, chatModel: ChatM val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId) if (r) { chatModel.removeChat(chatInfo.id) - chatModel.chatId.value = null - chatModel.controller.ntfManager.cancelNotificationsForChat(chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) close?.invoke() } } @@ -150,6 +161,9 @@ fun leaveGroupDialog(groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> U fun GroupChatInfoLayout( chat: Chat, groupInfo: GroupInfo, + currentUser: User, + sendReceipts: State<SendReceipts>, + setSendReceipts: (SendReceipts) -> Unit, members: List<GroupMember>, developerTools: Boolean, groupLink: String?, @@ -163,74 +177,92 @@ fun GroupChatInfoLayout( leaveGroup: () -> Unit, manageGroupLink: () -> Unit, ) { - Column( + val listState = rememberLazyListState() + val scope = rememberCoroutineScope() + KeyChangeEffect(chat.id) { + scope.launch { listState.scrollToItem(0) } + } + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } + val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } } + LazyColumn( Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) + .fillMaxWidth(), + state = listState ) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center - ) { - GroupChatInfoHeader(chat.chatInfo) - } - SectionSpacer() - - SectionView { - if (groupInfo.canEdit) { - EditGroupProfileButton(editGroupProfile) + item { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + GroupChatInfoHeader(chat.chatInfo) } - if (groupInfo.groupProfile.description != null || groupInfo.canEdit) { - AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) - } - GroupPreferencesButton(openPreferences) - } - SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) - SectionDividerSpaced(maxTopPadding = true) + SectionSpacer() - SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { - if (groupInfo.canAddMembers) { - if (groupLink == null) { - CreateGroupLinkButton(manageGroupLink) + SectionView { + if (groupInfo.canEdit) { + EditGroupProfileButton(editGroupProfile) + } + if (groupInfo.groupProfile.description != null || groupInfo.canEdit) { + AddOrEditWelcomeMessage(groupInfo.groupProfile.description, addOrEditWelcomeMessage) + } + GroupPreferencesButton(openPreferences) + if (members.filter { it.memberCurrent }.size <= SMALL_GROUPS_RCPS_MEM_LIMIT) { + SendReceiptsOption(currentUser, sendReceipts, setSendReceipts) } else { - GroupLinkButton(manageGroupLink) - } - - val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers - val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - AddMembersButton(tint, onAddMembersClick) - } - val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } - val filteredMembers = remember { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } } - if (members.size > 8) { - SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { - SearchRowView(searchText) + SendReceiptsOptionDisabled() } } - SectionItemView(minHeight = 54.dp) { - MemberRow(groupInfo.membership, user = true) - } - MembersList(filteredMembers.value, showMemberInfo) - } - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) - SectionView { - ClearChatButton(clearChat) - if (groupInfo.canDelete) { - DeleteGroupButton(deleteGroup) - } - if (groupInfo.membership.memberCurrent) { - LeaveGroupButton(leaveGroup) - } - } + SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) + SectionDividerSpaced(maxTopPadding = true) - if (developerTools) { - SectionDividerSpaced() - SectionView(title = stringResource(MR.strings.section_title_for_console)) { - InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName) - InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString()) + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), members.count() + 1)) { + if (groupInfo.canAddMembers) { + if (groupLink == null) { + CreateGroupLinkButton(manageGroupLink) + } else { + GroupLinkButton(manageGroupLink) + } + val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers + val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + AddMembersButton(tint, onAddMembersClick) + } + if (members.size > 8) { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + SearchRowView(searchText) + } + } + SectionItemView(minHeight = 54.dp) { + MemberRow(groupInfo.membership, user = true) + } } } - SectionBottomSpacer() + items(filteredMembers.value) { member -> + Divider() + SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) { + MemberRow(member) + } + } + item { + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionView { + ClearChatButton(clearChat) + if (groupInfo.canDelete) { + DeleteGroupButton(deleteGroup) + } + if (groupInfo.membership.memberCurrent) { + LeaveGroupButton(leaveGroup) + } + } + + if (developerTools) { + SectionDividerSpaced() + SectionView(title = stringResource(MR.strings.section_title_for_console)) { + InfoRow(stringResource(MR.strings.info_row_local_name), groupInfo.localDisplayName) + InfoRow(stringResource(MR.strings.info_row_database_id), groupInfo.apiId.toString()) + } + } + SectionBottomSpacer() + } } } @@ -269,6 +301,37 @@ private fun GroupPreferencesButton(onClick: () -> Unit) { ) } +@Composable +private fun SendReceiptsOption(currentUser: User, state: State<SendReceipts>, onSelected: (SendReceipts) -> Unit) { + val values = remember { + mutableListOf(SendReceipts.Yes, SendReceipts.No, SendReceipts.UserDefault(currentUser.sendRcptsSmallGroups)).map { it to it.text } + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.send_receipts), + values, + state, + icon = painterResource(MR.images.ic_double_check), + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) +} + +@Composable +fun SendReceiptsOptionDisabled() { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_double_check), + text = generalGetString(MR.strings.send_receipts), + click = { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.send_receipts_disabled_alert_title), + text = String.format(generalGetString(MR.strings.send_receipts_disabled_alert_msg), SMALL_GROUPS_RCPS_MEM_LIMIT) + ) + } + ) { + Text(generalGetString(MR.strings.send_receipts_disabled), color = MaterialTheme.colors.secondary) + } +} + @Composable private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick: () -> Unit) { SettingsActionItem( @@ -280,18 +343,6 @@ private fun AddMembersButton(tint: Color = MaterialTheme.colors.primary, onClick ) } -@Composable -private fun MembersList(members: List<GroupMember>, showMemberInfo: (GroupMember) -> Unit) { - Column { - members.forEachIndexed { index, member -> - Divider() - SectionItemView({ showMemberInfo(member) }, minHeight = 54.dp) { - MemberRow(member) - } - } - } -} - @Composable private fun MemberRow(member: GroupMember, user: Boolean = false) { Row( @@ -429,6 +480,9 @@ fun PreviewGroupChatInfoLayout() { chatItems = arrayListOf() ), groupInfo = GroupInfo.sampleData, + User.sampleData, + sendReceipts = remember { mutableStateOf(SendReceipts.Yes) }, + setSendReceipts = {}, members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, groupLink = null, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt similarity index 90% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 025251cd8b..7c767f9b7e 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import SectionBottomSpacer import androidx.compose.foundation.layout.* @@ -10,19 +10,20 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCode +import chat.simplex.common.model.* +import chat.simplex.common.platform.shareText +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode import chat.simplex.res.MR @Composable -fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String?, GroupMemberRole?>) -> Unit) { +fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: String?, memberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit) { var groupLink by rememberSaveable { mutableStateOf(connReqContact) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } @@ -33,7 +34,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St if (link != null) { groupLink = link.first groupLinkMemberRole.value = link.second - onGroupLinkUpdated(groupLink to groupLinkMemberRole.value) + onGroupLinkUpdated(link) } creatingLink = false } @@ -43,13 +44,14 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St createLink() } } + val clipboard = LocalClipboardManager.current GroupLinkLayout( groupLink = groupLink, groupInfo, groupLinkMemberRole, creatingLink, createLink = ::createLink, - share = { shareText(groupLink ?: return@GroupLinkLayout) }, + share = { clipboard.shareText(groupLink ?: return@GroupLinkLayout) }, updateLink = { val role = groupLinkMemberRole.value if (role != null) { @@ -58,7 +60,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St if (link != null) { groupLink = link.first groupLinkMemberRole.value = link.second - onGroupLinkUpdated(groupLink to groupLinkMemberRole.value) + onGroupLinkUpdated(link) } } } @@ -73,7 +75,7 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St val r = chatModel.controller.apiDeleteGroupLink(groupInfo.groupId) if (r) { groupLink = null - onGroupLinkUpdated(null to null) + onGroupLinkUpdated(null) } } }, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt similarity index 80% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index df36cf9aff..e14089ec52 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -1,14 +1,14 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer import SectionDividerSpaced +import SectionItemView import SectionSpacer import SectionTextFooter import SectionView -import android.net.Uri -import android.util.Log -import androidx.activity.compose.BackHandler +import androidx.compose.desktop.ui.tooling.preview.Preview +import java.net.URI import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.InlineTextContent @@ -18,23 +18,24 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource 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.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.* -import chat.simplex.app.views.usersettings.SettingsActionItem +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.common.model.GroupInfo +import chat.simplex.common.platform.* +import chat.simplex.common.views.chatlist.openChat import chat.simplex.res.MR import kotlinx.datetime.Clock @@ -52,6 +53,8 @@ fun GroupMemberInfoView( val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() + var progressIndicator by remember { mutableStateOf(false) } + if (chat != null) { val newRole = remember { mutableStateOf(member.memberRole) } GroupMemberInfoLayout( @@ -76,15 +79,23 @@ fun GroupMemberInfoView( } } }, - connectViaAddress = { connReqUri -> - val uri = Uri.parse(connReqUri) - withUriAction(uri) { linkType -> - withApi { - Log.d(TAG, "connectViaUri: connecting") - connectViaUri(chatModel, linkType, uri) + createMemberContact = { + withApi { + progressIndicator = true + val memberContact = chatModel.controller.apiCreateMemberContact(groupInfo.apiId, member.groupMemberId) + if (memberContact != null) { + val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf()) + chatModel.addChat(memberChat) + openChat(memberChat, chatModel) + closeAll() + chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } + progressIndicator = false } }, + connectViaAddress = { connReqUri -> + connectViaMemberAddressAlert(connReqUri) + }, removeMember = { removeMemberDialog(groupInfo, member, chatModel, close) }, onRoleSelected = { if (it == newRole.value) return@GroupMemberInfoLayout @@ -150,7 +161,7 @@ fun GroupMemberInfoView( }) }, verifyClicked = { - ModalManager.shared.showModalCloseable { close -> + ModalManager.end.showModalCloseable { close -> remember { derivedStateOf { chatModel.groupMembers.firstOrNull { it.memberId == member.memberId } } }.value?.let { mem -> VerifyCodeView( mem.displayName, @@ -176,6 +187,10 @@ fun GroupMemberInfoView( } } ) + + if (progressIndicator) { + ProgressIndicator() + } } } @@ -207,6 +222,7 @@ fun GroupMemberInfoLayout( connectionCode: String?, getContactChat: (Long) -> Chat?, openDirectChat: (Long) -> Unit, + createMemberContact: () -> Unit, connectViaAddress: (String) -> Unit, removeMember: () -> Unit, onRoleSelected: (GroupMemberRole) -> Unit, @@ -243,9 +259,13 @@ fun GroupMemberInfoLayout( if (member.memberActive) { SectionView { - if (contactId != null) { - if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) { + if (contactId != null && knownDirectChat(contactId) != null) { + OpenChatButton(onClick = { openDirectChat(contactId) }) + } else if (groupInfo.fullGroupPreferences.directMessages.on) { + if (contactId != null) { OpenChatButton(onClick = { openDirectChat(contactId) }) + } else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) { + OpenChatButton(onClick = { createMemberContact() }) } } if (connectionCode != null) { @@ -262,10 +282,10 @@ fun GroupMemberInfoLayout( } if (member.contactLink != null) { - val context = LocalContext.current SectionView(stringResource(MR.strings.address_section_title).uppercase()) { QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { shareText(member.contactLink) } + val clipboard = LocalClipboardManager.current + ShareAddressButton { clipboard.shareText(member.contactLink) } if (contactId != null) { if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) { ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) }) @@ -451,6 +471,46 @@ private fun updateMemberRoleDialog( ) } +fun connectViaMemberAddressAlert(connReqUri: String) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.connect_via_member_address_alert_title), + text = AnnotatedString(generalGetString(MR.strings.connect_via_member_address_alert_desc)), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + val uri = URI(connReqUri) + withUriAction(uri) { linkType -> + withApi { + Log.d(TAG, "connectViaUri: connecting") + connectViaUri(chatModel, linkType, uri, incognito = false) + } + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + val uri = URI(connReqUri) + withUriAction(uri) { linkType -> + withApi { + Log.d(TAG, "connectViaUri: connecting incognito") + connectViaUri(chatModel, linkType, uri, incognito = true) + } + } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + @Preview @Composable fun PreviewGroupMemberInfoLayout() { @@ -464,6 +524,7 @@ fun PreviewGroupMemberInfoLayout() { connectionCode = "123", getContactChat = { Chat.sampleData }, openDirectChat = {}, + createMemberContact = {}, connectViaAddress = {}, removeMember = {}, onRoleSelected = {}, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt similarity index 96% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index 1801a0ff75..d95d2a4cff 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer @@ -14,11 +14,10 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.PreferenceToggleWithIcon +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon +import chat.simplex.common.model.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt similarity index 90% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index 5031babd49..ac13dd483a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -1,8 +1,7 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import SectionBottomSpacer -import android.content.res.Configuration -import android.net.Uri +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,22 +14,20 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.ProfileNameField -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.isValidDisplayName -import chat.simplex.app.views.onboarding.ReadableText -import chat.simplex.app.views.usersettings.* -import com.google.accompanist.insets.ProvideWindowInsets -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.ProfileNameField +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.isValidDisplayName +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.net.URI @Composable fun GroupProfileView(groupInfo: GroupInfo, chatModel: ChatModel, close: () -> Unit) { @@ -58,7 +55,7 @@ fun GroupProfileLayout( val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = rememberSaveable { mutableStateOf(groupProfile.displayName) } val fullName = rememberSaveable { mutableStateOf(groupProfile.fullName) } - val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) } + val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) } val profileImage = rememberSaveable { mutableStateOf(groupProfile.image) } val scope = rememberCoroutineScope() val scrollState = rememberScrollState() @@ -191,12 +188,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewGroupProfileLayout() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 7cd7c9ca38..3be54376d5 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.group +package chat.simplex.common.views.chat.group import SectionBottomSpacer import SectionDividerSpaced @@ -14,16 +14,18 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.chat.item.MarkdownText -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.chat.item.MarkdownText +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.GroupInfo import chat.simplex.res.MR import kotlinx.coroutines.delay @@ -99,15 +101,17 @@ private fun GroupWelcomeLayout( }, wt.value.isEmpty() ) - CopyTextButton { copyText(wt.value) } + val clipboard = LocalClipboardManager.current + CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } SectionDividerSpaced(maxBottomPadding = false) SaveButton( save = save, disabled = wt.value == groupInfo.groupProfile.description || (wt.value == "" && groupInfo.groupProfile.description == null) ) } else { + val clipboard = LocalClipboardManager.current TextPreview(wt.value, linkMode) - CopyTextButton { copyText(wt.value) } + CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } } SectionBottomSpacer() } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt index b2f2245d23..3e4dce7f4a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CICallItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -10,9 +10,9 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.SimpleButton import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIChatFeatureView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt similarity index 88% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIChatFeatureView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt index 752cf8681b..e919ab1aa8 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIChatFeatureView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIChatFeatureView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -9,7 +9,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* +import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.Feature @Composable fun CIChatFeatureView( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt new file mode 100644 index 0000000000..508116f7e3 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIEventView.kt @@ -0,0 +1,33 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.ui.theme.* + +@Composable +fun CIEventView(text: AnnotatedString) { + Row( + Modifier.padding(horizontal = 6.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)) + } +} +@Preview/*( + uiMode = Configuration.UI_MODE_NIGHT_YES, + name = "Dark Mode" +)*/ +@Composable +fun CIEventViewPreview() { + SimpleXTheme { + CIEventView(buildAnnotatedString { append("event happened") }) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFeaturePreferenceView.kt similarity index 93% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFeaturePreferenceView.kt index cdb261d58b..1a23dfc49a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIFeaturePreferenceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFeaturePreferenceView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -9,9 +9,8 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.model.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt similarity index 87% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index 82ecefaf20..87f4aa4f31 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.widget.Toast import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -12,27 +11,25 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.datetime.Clock +import java.io.File +import java.net.URI @Composable fun CIFileView( file: CIFile?, edited: Boolean, - receiveFile: (Long) -> Unit + receiveFile: (Long, Boolean) -> Unit ) { - val context = LocalContext.current val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) @Composable @@ -74,7 +71,8 @@ fun CIFileView( when (file.fileStatus) { is CIFileStatus.RcvInvitation -> { if (fileSizeValid()) { - receiveFile(file.fileId) + val encrypted = chatController.appPrefs.privacyEncryptLocalFiles.get() + receiveFile(file.fileId, encrypted) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), @@ -98,9 +96,11 @@ fun CIFileView( is CIFileStatus.RcvComplete -> { val filePath = getLoadedFilePath(file) if (filePath != null) { - saveFileLauncher.launch(file.fileName) + withApi { + saveFileLauncher.launch(file.fileName) + } } else { - Toast.makeText(context, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show() + showToast(generalGetString(MR.strings.file_not_found)) } } else -> {} @@ -185,9 +185,9 @@ fun CIFileView( ) { fileIndicator() val metaReserve = if (edited) - " " + " " else - " " + " " if (file != null) { Column { Text( @@ -207,6 +207,24 @@ fun CIFileView( } } +@Composable +fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = + rememberFileChooserLauncher(false, ciFile) { to: URI? -> + val filePath = getLoadedFilePath(ciFile) + if (filePath != null && to != null) { + if (ciFile?.fileSource?.cryptoArgs != null) { + createTmpFileAndDelete { tmpFile -> + decryptCryptoFile(filePath, ciFile.fileSource.cryptoArgs, tmpFile.absolutePath) + copyFileToFile(tmpFile, to) {} + tmpFile.delete() + } + } else { + copyFileToFile(File(filePath), to) {} + } + } + } + +/* class ChatItemProvider: PreviewParameterProvider<ChatItem> { private val sentFile = ChatItem( chatDir = CIDirection.DirectSnd(), @@ -245,4 +263,4 @@ fun PreviewCIFileFramedItemView(@PreviewParameter(ChatItemProvider::class) chatI SimpleXTheme { FramedItemView(ChatInfo.Direct.sampleData, chatItem, linkMode = SimplexLinkMode.DESCRIPTION, showMenu = showMenu, receiveFile = {}) } -} +}*/ diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt similarity index 93% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index d66dc4ae23..6ee29b8304 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -1,6 +1,6 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.content.res.Configuration +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -11,13 +11,11 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* import chat.simplex.res.MR @Composable @@ -115,11 +113,10 @@ fun CIGroupInvitationView( } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" -) +)*/ @Composable fun PendingCIGroupInvitationViewPreview() { SimpleXTheme { @@ -132,11 +129,10 @@ fun PendingCIGroupInvitationViewPreview() { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" -) +)*/ @Composable fun CIGroupInvitationViewAcceptedPreview() { SimpleXTheme { @@ -149,7 +145,7 @@ fun CIGroupInvitationViewAcceptedPreview() { } } -@Preview(showBackground = true) +@Preview @Composable fun CIGroupInvitationViewLongNamePreview() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt similarity index 66% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index f5ebf4753b..23d1f1d0cc 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -1,18 +1,13 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.graphics.Bitmap -import android.os.Build.VERSION.SDK_INT -import androidx.compose.foundation.Image -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layoutId @@ -21,27 +16,24 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.core.content.FileProvider -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.views.helpers.* -import coil.ImageLoader -import coil.compose.rememberAsyncImagePainter -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import coil.request.ImageRequest +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import java.io.File +import java.net.URI @Composable fun CIImageView( image: String, file: CIFile?, + encryptLocalFile: Boolean, + metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState<Boolean>, - receiveFile: (Long) -> Unit + receiveFile: (Long, Boolean) -> Unit ) { @Composable fun progressIndicator() { @@ -58,7 +50,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = Color.White + tint = metaColor ) } @@ -96,39 +88,41 @@ fun CIImageView( @Composable fun imageViewFullWidth(): Dp { val approximatePadding = 100.dp - return with(LocalDensity.current) { minOf(1000.dp, LocalView.current.width.toDp() - approximatePadding) } + return with(LocalDensity.current) { minOf(DEFAULT_MAX_IMAGE_WIDTH, LocalWindowWidth() - approximatePadding) } } @Composable - fun imageView(imageBitmap: Bitmap, onClick: () -> Unit) { + fun imageView(imageBitmap: ImageBitmap, onClick: () -> Unit) { Image( - imageBitmap.asImageBitmap(), + imageBitmap, contentDescription = stringResource(MR.strings.image_descr), - // .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView + // .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView // if text is short and take all available width if text is long modifier = Modifier - .width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else 1000.dp) + .width(if (imageBitmap.width * 0.97 <= imageBitmap.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) .combinedClickable( onLongClick = { showMenu.value = true }, onClick = onClick - ), + ) + .onRightClick { showMenu.value = true }, contentScale = ContentScale.FillWidth, ) } @Composable - fun imageView(painter: Painter, onClick: () -> Unit) { + fun ImageView(painter: Painter, onClick: () -> Unit) { Image( painter, contentDescription = stringResource(MR.strings.image_descr), - // .width(1000.dp) is a hack for image to increase IntrinsicSize of FramedItemView + // .width(DEFAULT_MAX_IMAGE_WIDTH) is a hack for image to increase IntrinsicSize of FramedItemView // if text is short and take all available width if text is long modifier = Modifier - .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else 1000.dp) + .width(if (painter.intrinsicSize.width * 0.97 <= painter.intrinsicSize.height) imageViewFullWidth() * 0.75f else DEFAULT_MAX_IMAGE_WIDTH) .combinedClickable( onLongClick = { showMenu.value = true }, onClick = onClick - ), + ) + .onRightClick { showMenu.value = true }, contentScale = ContentScale.FillWidth, ) } @@ -140,41 +134,31 @@ fun CIImageView( return false } - fun imageAndFilePath(file: CIFile?): Pair<Bitmap?, String?> { - val imageBitmap: Bitmap? = getLoadedImage(file) - val filePath = getLoadedFilePath(file) - return imageBitmap to filePath + fun imageAndFilePath(file: CIFile?): Triple<ImageBitmap, ByteArray, String>? { + val res = getLoadedImage(file) + if (res != null) { + val (imageBitmap: ImageBitmap, data: ByteArray) = res + val filePath = getLoadedFilePath(file)!! + return Triple(imageBitmap, data, filePath) + } + return null } Box( Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), contentAlignment = Alignment.TopEnd ) { - val context = LocalContext.current - val (imageBitmap, filePath) = remember(file) { imageAndFilePath(file) } - if (imageBitmap != null && filePath != null) { - val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) } - val imagePainter = rememberAsyncImagePainter( - ImageRequest.Builder(context).data(data = uri).size(coil.size.Size.ORIGINAL).build(), - placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil - imageLoader = imageLoader - ) - val view = LocalView.current - imageView(imagePainter, onClick = { - hideKeyboard(view) - if (getLoadedFilePath(file) != null) { - ModalManager.shared.showCustomModal(animated = false) { close -> - ImageFullScreenView(imageProvider, close) - } - } - }) + val res = remember(file) { imageAndFilePath(file) } + if (res != null) { + val (imageBitmap, data, _) = res + SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, onClick) }) } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { when (file.fileStatus) { CIFileStatus.RcvInvitation -> if (fileSizeValid()) { - receiveFile(file.fileId) + receiveFile(file.fileId, encryptLocalFile) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), @@ -206,12 +190,11 @@ fun CIImageView( } } -private val imageLoader = ImageLoader.Builder(SimplexApp.context) - .components { - if (SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() +@Composable +expect fun SimpleAndAnimatedImageView( + data: ByteArray, + imageBitmap: ImageBitmap, + file: CIFile?, + imageProvider: () -> ImageGalleryProvider, + ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit +) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt similarity index 65% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt index 64111ee4ea..21c27fa34e 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIInvalidJSONView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIInvalidJSONView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import SectionView import androidx.compose.foundation.* @@ -7,20 +7,25 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.SettingsActionItem +import chat.simplex.common.platform.shareText +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR @Composable fun CIInvalidJSONView(json: String) { Row(Modifier - .clickable { ModalManager.shared.showModal(true) { InvalidJSONView(json) } } + .clickable { + ModalManager.center.closeModals() + ModalManager.end.closeModals() + ModalManager.center.showModal(true) { InvalidJSONView(json) } + } .padding(horizontal = 10.dp, vertical = 6.dp) ) { Text(stringResource(MR.strings.invalid_data), color = Color.Red, fontStyle = FontStyle.Italic) @@ -32,8 +37,9 @@ fun InvalidJSONView(json: String) { Column { Spacer(Modifier.height(DEFAULT_PADDING)) SectionView { + val clipboard = LocalClipboardManager.current SettingsActionItem(painterResource(MR.images.ic_share), generalGetString(MR.strings.share_verb), click = { - shareText(json) + clipboard.shareText(json) }) } Column(Modifier.padding(DEFAULT_PADDING).fillMaxWidth().verticalScroll(rememberScrollState())) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt new file mode 100644 index 0000000000..2ade49b3fc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt @@ -0,0 +1,70 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.model.* +import chat.simplex.res.MR + +@Composable +fun CIMemberCreatedContactView( + chatItem: ChatItem, + openDirectChat: (Long) -> Unit +) { + fun eventText(): AnnotatedString { + val memberDisplayName = chatItem.memberDisplayName + return if (memberDisplayName != null) { + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + withStyle(chatEventStyle) { append(chatItem.content.text) } + } + } else { + buildAnnotatedString { + withStyle(chatEventStyle) { append(chatItem.content.text) } + } + } + } + + Row( + Modifier.padding(horizontal = 6.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (chatItem.chatDir is CIDirection.GroupRcv && chatItem.chatDir.groupMember.memberContactId != null) { + val openChatStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp) + val annotatedText = buildAnnotatedString { + append(eventText()) + append(" ") + withAnnotation(tag = "Open", annotation = "Open") { + withStyle(openChatStyle) { append(generalGetString(MR.strings.rcv_group_event_open_chat) + " ") } + } + withStyle(chatEventStyle) { append(chatItem.timestampText) } + } + + fun open(offset: Int): Boolean = annotatedText.getStringAnnotations(tag = "Open", start = offset, end = offset).isNotEmpty() + ClickableText( + annotatedText, + onClick = { + if (open(it)) { + openDirectChat(chatItem.chatDir.groupMember.memberContactId) + } + }, + shouldConsumeEvent = ::open + ) + } else { + val annotatedText = buildAnnotatedString { + append(eventText()) + append(" ") + withStyle(chatEventStyle) { append(chatItem.timestampText) } + } + Text(annotatedText) + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt similarity index 74% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index 592f8e6b6d..72f7137b55 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -9,15 +9,32 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.CurrentColors +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.isInDarkTheme import chat.simplex.res.MR import kotlinx.datetime.Clock @Composable -fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = MaterialTheme.colors.secondary) { +fun CIMetaView( + chatItem: ChatItem, + timedMessagesTTL: Int?, + metaColor: Color = MaterialTheme.colors.secondary, + paleMetaColor: Color = if (isInDarkTheme()) { + metaColor.copy( + red = metaColor.red * 0.67F, + green = metaColor.green * 0.67F, + blue = metaColor.red * 0.67F) + } else { + metaColor.copy( + red = minOf(metaColor.red * 1.33F, 1F), + green = minOf(metaColor.green * 1.33F, 1F), + blue = minOf(metaColor.red * 1.33F, 1F)) + } +) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { Text( @@ -27,14 +44,14 @@ fun CIMetaView(chatItem: ChatItem, timedMessagesTTL: Int?, metaColor: Color = Ma modifier = Modifier.padding(start = 3.dp) ) } else { - CIMetaText(chatItem.meta, timedMessagesTTL, metaColor) + CIMetaText(chatItem.meta, timedMessagesTTL, encrypted = chatItem.encryptedFile, metaColor, paleMetaColor) } } } @Composable // changing this function requires updating reserveSpaceForMeta -private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) { +private fun CIMetaText(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, color: Color, paleColor: Color) { if (meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) Spacer(Modifier.width(3.dp)) @@ -47,7 +64,7 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) { } Spacer(Modifier.width(4.dp)) } - val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color) + val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) if (statusIcon != null) { val (icon, statusColor) = statusIcon if (meta.itemStatus is CIStatus.SndSent || meta.itemStatus is CIStatus.SndRcvd) { @@ -60,11 +77,15 @@ private fun CIMetaText(meta: CIMeta, chatTTL: Int?, color: Color) { StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent) Spacer(Modifier.width(4.dp)) } + if (encrypted != null) { + StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color) + Spacer(Modifier.width(4.dp)) + } Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) } // the conditions in this function should match CIMetaText -fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String { +fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?): String { val iconSpace = " " var res = "" if (meta.itemEdited) res += iconSpace @@ -78,6 +99,9 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?): String { if (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing) { res += iconSpace } + if (encrypted != null) { + res += iconSpace + } return res + meta.timestampText } @@ -137,7 +161,7 @@ fun PreviewCIMetaViewSendNoAuth() { fun PreviewCIMetaViewSendSent() { CIMetaView( chatItem = ChatItem.getSampleData( - 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent() + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete) ), null ) @@ -162,7 +186,7 @@ fun PreviewCIMetaViewEditedUnread() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectRcv(), Clock.System.now(), "hello", itemEdited = true, - status=CIStatus.RcvNew() + status= CIStatus.RcvNew() ), null ) @@ -175,7 +199,7 @@ fun PreviewCIMetaViewEditedSent() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = true, - status=CIStatus.SndSent() + status= CIStatus.SndSent(SndCIStatusProgress.Complete) ), null ) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt similarity index 93% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIRcvDecryptionError.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 060c94028e..8de309fc8f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -10,14 +10,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle -import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.CurrentColors -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource @Composable fun CIRcvDecryptionError( @@ -31,7 +32,6 @@ fun CIRcvDecryptionError( syncMemberConnection: (GroupInfo, GroupMember) -> Unit, findModelChat: (String) -> Chat?, findModelMember: (String) -> GroupMember?, - showMember: Boolean ) { LaunchedEffect(Unit) { if (cInfo is ChatInfo.Direct) { @@ -45,7 +45,6 @@ fun CIRcvDecryptionError( fun BasicDecryptionErrorItem() { DecryptionErrorItem( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.decryption_error), @@ -63,7 +62,6 @@ fun CIRcvDecryptionError( if (modelContactStats.ratchetSyncAllowed) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.fix_connection_question), @@ -77,7 +75,6 @@ fun CIRcvDecryptionError( } else if (!modelContactStats.ratchetSyncSupported) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.fix_connection_not_supported_by_contact), @@ -102,7 +99,6 @@ fun CIRcvDecryptionError( if (modelMemberStats.ratchetSyncAllowed) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.fix_connection_question), @@ -116,7 +112,6 @@ fun CIRcvDecryptionError( } else if (!modelMemberStats.ratchetSyncSupported) { DecryptionErrorItemFixButton( ci, - showMember, onClick = { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.fix_connection_not_supported_by_group_member), @@ -139,7 +134,6 @@ fun CIRcvDecryptionError( @Composable fun DecryptionErrorItemFixButton( ci: ChatItem, - showMember: Boolean, onClick: () -> Unit, syncSupported: Boolean ) { @@ -158,7 +152,6 @@ fun DecryptionErrorItemFixButton( ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) @@ -173,7 +166,7 @@ fun DecryptionErrorItemFixButton( Text( buildAnnotatedString { append(generalGetString(MR.strings.fix_connection)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } withStyle(reserveTimestampStyle) { append(" ") } // for icon }, color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary @@ -188,7 +181,6 @@ fun DecryptionErrorItemFixButton( @Composable fun DecryptionErrorItem( ci: ChatItem, - showMember: Boolean, onClick: () -> Unit ) { val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -203,9 +195,8 @@ fun DecryptionErrorItem( ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) ) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIVideoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt similarity index 77% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIVideoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt index ee8422d46d..78bdf53d14 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIVideoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt @@ -1,8 +1,5 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.graphics.Bitmap -import android.graphics.Rect -import android.net.Uri import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -11,27 +8,21 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.* import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.* -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.content.FileProvider -import androidx.core.graphics.drawable.toDrawable -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH -import com.google.android.exoplayer2.ui.StyledPlayerView import chat.simplex.res.MR +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* import dev.icerock.moko.resources.StringResource import java.io.File +import java.net.URI @Composable fun CIVideoView( @@ -40,31 +31,30 @@ fun CIVideoView( file: CIFile?, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState<Boolean>, - receiveFile: (Long) -> Unit + receiveFile: (Long, Boolean) -> Unit ) { Box( Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), contentAlignment = Alignment.TopEnd ) { - val context = LocalContext.current val filePath = remember(file) { getLoadedFilePath(file) } val preview = remember(image) { base64ToBitmap(image) } if (file != null && filePath != null) { - val uri = remember(filePath) { FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", File(filePath)) } - val view = LocalView.current + val uri = remember(filePath) { getAppFileUri(filePath.substringAfterLast(File.separator)) } + val view = LocalMultiplatformView() VideoView(uri, file, preview, duration * 1000L, showMenu, onClick = { hideKeyboard(view) - ModalManager.shared.showCustomModal(animated = false) { close -> + ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) } }) } else { Box { - ImageView(preview, showMenu, onClick = { + VideoPreviewImageView(preview, onClick = { if (file != null) { when (file.fileStatus) { CIFileStatus.RcvInvitation -> - receiveFileIfValidSize(file, receiveFile) + receiveFileIfValidSize(file, encrypted = false, receiveFile) CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> @@ -85,12 +75,15 @@ fun CIVideoView( else -> {} } } - }) + }, + onLongClick = { + showMenu.value = true + }) if (file != null) { DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } + PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, encrypted = false, receiveFile) } } } } @@ -99,14 +92,13 @@ fun CIVideoView( } @Composable -private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) { - val context = LocalContext.current - val player = remember(uri) { VideoPlayer.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } +private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, showMenu: MutableState<Boolean>, onClick: () -> Unit) { + val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } val videoPlaying = remember(uri.path) { player.videoPlaying } val progress = remember(uri.path) { player.progress } val duration = remember(uri.path) { player.duration } val preview by remember { player.preview } -// val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } + // val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo } val play = { player.enableSound(true) @@ -122,32 +114,28 @@ private fun VideoView(uri: Uri, file: CIFile, defaultPreview: Bitmap, defaultDur stop() } } + val onLongClick = { showMenu.value = true } Box { val windowWidth = LocalWindowWidth() - val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp } - AndroidView( - factory = { ctx -> - StyledPlayerView(ctx).apply { - useController = false - resizeMode = RESIZE_MODE_FIXED_WIDTH - this.player = player.player - } - }, - Modifier - .width(width) - .combinedClickable( - onLongClick = { showMenu.value = true }, - onClick = { if (player.player.playWhenReady) stop() else onClick() } - ) + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } + PlayerView( + player, + width, + onClick = onClick, + onLongClick = onLongClick, + stop ) if (showPreview.value) { - ImageView(preview, showMenu, onClick) - PlayButton(brokenVideo, onLongClick = { showMenu.value = true }, play) + VideoPreviewImageView(preview, onClick, onLongClick) + PlayButton(brokenVideo, onLongClick = onLongClick, if (appPlatform.isAndroid) play else onClick) } DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/) } } +@Composable +expect fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) + @Composable private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) { Surface( @@ -158,7 +146,8 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, Box( Modifier .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) - .combinedClickable(onClick = onClick, onLongClick = onLongClick), + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .onRightClick { onLongClick.invoke() }, contentAlignment = Alignment.Center ) { Icon( @@ -216,32 +205,25 @@ private fun DurationProgress(file: CIFile, playing: MutableState<Boolean>, durat } @Composable -private fun ImageView(preview: Bitmap, showMenu: MutableState<Boolean>, onClick: () -> Unit) { +fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { val windowWidth = LocalWindowWidth() - val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else 1000.dp } + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } Image( - preview.asImageBitmap(), + preview, contentDescription = stringResource(MR.strings.video_descr), modifier = Modifier .width(width) .combinedClickable( - onLongClick = { showMenu.value = true }, + onLongClick = onLongClick, onClick = onClick - ), + ) + .onRightClick(onLongClick), contentScale = ContentScale.FillWidth, ) } @Composable -private fun LocalWindowWidth(): Dp { - val view = LocalView.current - val density = LocalDensity.current.density - return remember { - val rect = Rect() - view.getWindowVisibleDisplayFrame(rect) - (rect.width() / density).dp - } -} +expect fun LocalWindowWidth(): Dp @Composable private fun progressIndicator() { @@ -323,9 +305,9 @@ private fun fileSizeValid(file: CIFile?): Boolean { return false } -private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) { +private fun receiveFileIfValidSize(file: CIFile, encrypted: Boolean, receiveFile: (Long, Boolean) -> Unit) { if (fileSizeValid(file)) { - receiveFile(file.fileId) + receiveFile(file.fileId, encrypted) } else { AlertManager.shared.showAlertMsg( generalGetString(MR.strings.large_file), @@ -336,5 +318,5 @@ private fun receiveFileIfValidSize(file: CIFile, receiveFile: (Long) -> Unit) { private fun videoViewFullWidth(windowWidth: Dp): Dp { val approximatePadding = 100.dp - return minOf(1000.dp, windowWidth - approximatePadding) + return minOf(DEFAULT_MAX_IMAGE_WIDTH, windowWidth - approximatePadding) } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index 8d331c2276..941bc315b6 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* @@ -15,12 +15,13 @@ import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.* -import androidx.compose.ui.unit.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource +import androidx.compose.ui.unit.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 @@ -35,22 +36,24 @@ fun CIVoiceView( ci: ChatItem, timedMessagesTTL: Int?, longClick: () -> Unit, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, ) { Row( Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp), verticalAlignment = Alignment.CenterVertically ) { if (file != null) { - val context = LocalContext.current - val filePath = remember(file.filePath, file.fileStatus) { getLoadedFilePath(file) } - var brokenAudio by rememberSaveable(file.filePath) { mutableStateOf(false) } - val audioPlaying = rememberSaveable(file.filePath) { mutableStateOf(false) } - val progress = rememberSaveable(file.filePath) { mutableStateOf(0) } - val duration = rememberSaveable(file.filePath) { mutableStateOf(providedDurationSec * 1000) } + val f = file.fileSource?.filePath + val fileSource = remember(f, file.fileStatus) { getLoadedFileSource(file) } + var brokenAudio by rememberSaveable(f) { mutableStateOf(false) } + val audioPlaying = rememberSaveable(f) { mutableStateOf(false) } + val progress = rememberSaveable(f) { mutableStateOf(0) } + val duration = rememberSaveable(f) { mutableStateOf(providedDurationSec * 1000) } val play = { - AudioPlayer.play(filePath, audioPlaying, progress, duration, true) - brokenAudio = !audioPlaying.value + if (fileSource != null) { + AudioPlayer.play(fileSource, audioPlaying, progress, duration, true) + brokenAudio = !audioPlaying.value + } } val pause = { AudioPlayer.pause(audioPlaying, progress) @@ -65,7 +68,7 @@ fun CIVoiceView( } } VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { - AudioPlayer.seekTo(it, progress, filePath) + AudioPlayer.seekTo(it, progress, fileSource?.filePath) } } else { VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile) @@ -93,7 +96,7 @@ private fun VoiceLayout( play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, onProgressChanged: (Int) -> Unit, ) { @Composable @@ -107,7 +110,7 @@ private fun VoiceLayout( MaterialTheme.colors.primary.mixWith( backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha), 0.24f) - val width = with(LocalDensity.current) { LocalView.current.width.toDp() } + val width = LocalWindowWidth() val colors = SliderDefaults.colors( inactiveTrackColor = inactiveTrackColor ) @@ -221,7 +224,8 @@ private fun PlayPauseButton( .combinedClickable( onClick = { if (!audioPlaying) play() else pause() }, onLongClick = longClick - ), + ) + .onRightClick { longClick() }, contentAlignment = Alignment.Center ) { Icon( @@ -246,7 +250,7 @@ private fun VoiceMsgIndicator( play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, ) { val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } val strokeColor = MaterialTheme.colors.primary @@ -266,7 +270,7 @@ private fun VoiceMsgIndicator( } } else { if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId, chatController.appPrefs.privacyEncryptLocalFiles.get()) }, {}, longClick = longClick) } else if (file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted ) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt similarity index 79% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 4829e05b2e..e1d45ffb31 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.Manifest -import android.os.Build +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,38 +13,48 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* +import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.* -import chat.simplex.app.views.helpers.* -import com.google.accompanist.permissions.rememberPermissionState +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.helpers.* +import chat.simplex.common.views.chat.ComposeContextItem +import chat.simplex.common.views.chat.ComposeState import chat.simplex.res.MR import kotlinx.datetime.Clock // TODO refactor so that FramedItemView can show all CIContent items if they're deleted (see Swift code) +val chatEventStyle = SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.Light, color = CurrentColors.value.colors.secondary) + +fun chatEventText(ci: ChatItem): AnnotatedString = + chatEventText(ci.content.text, ci.timestampText) + +fun chatEventText(eventText: String, ts: String): AnnotatedString = + buildAnnotatedString { + withStyle(chatEventStyle) { append("$eventText $ts") } + } + @Composable fun ChatItemView( cInfo: ChatInfo, cItem: ChatItem, composeState: MutableState<ComposeState>, imageProvider: (() -> ImageGalleryProvider)? = null, - showMember: Boolean = false, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, deleteMessage: (Long, CIDeleteMode) -> Unit, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, cancelFile: (Long) -> Unit, joinGroup: (Long) -> Unit, acceptCall: (Contact) -> Unit, scrollToItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, + openDirectChat: (Long) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -54,15 +63,15 @@ fun ChatItemView( findModelMember: (String) -> GroupMember?, setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, + getConnectedMemberNames: (() -> List<String>)? = null, + developerTools: Boolean, ) { - val context = LocalContext.current val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart val showMenu = remember { mutableStateOf(false) } val revealed = remember { mutableStateOf(false) } val fullDeleteAllowed = remember(cInfo) { cInfo.featureEnabled(ChatFeature.FullDelete) } - val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) val onLinkLongClick = { _: String -> showMenu.value = true } val live = composeState.value.liveMessage != null @@ -86,7 +95,7 @@ fun ChatItemView( @Composable fun ChatItemReactions() { - Row { + Row(verticalAlignment = Alignment.CenterVertically) { cItem.reactions.forEach { r -> var modifier = Modifier.padding(horizontal = 5.dp, vertical = 2.dp).clip(RoundedCornerShape(8.dp)) if (cInfo.featureEnabled(ChatFeature.Reactions) && (cItem.allowAddReaction || r.userReacted)) { @@ -95,13 +104,15 @@ fun ChatItemView( } } Row(modifier.padding(2.dp)) { - Text(r.reaction.text, fontSize = 12.sp) + ReactionIcon(r.reaction.text, fontSize = 12.sp) if (r.totalReacted > 1) { Spacer(Modifier.width(4.dp)) - Text("${r.totalReacted}", + Text( + "${r.totalReacted}", fontSize = 11.5.sp, fontWeight = if (r.userReacted) FontWeight.Bold else FontWeight.Normal, color = if (r.userReacted) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + modifier = if (appPlatform.isAndroid) Modifier else Modifier.padding(top = 4.dp) ) } } @@ -113,11 +124,12 @@ fun ChatItemView( Column( Modifier .clip(RoundedCornerShape(18.dp)) - .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick), + .combinedClickable(onLongClick = { showMenu.value = true }, onClick = onClick) + .onRightClick { showMenu.value = true }, ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, showMember = showMember, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) } fun deleteMessageQuestionText(): String { @@ -146,7 +158,7 @@ fun ChatItemView( } } if (rs.isNotEmpty()) { - Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState())) { + Row(modifier = Modifier.padding(horizontal = DEFAULT_PADDING).horizontalScroll(rememberScrollState()), verticalAlignment = Alignment.CenterVertically) { rs.forEach() { r -> Box( Modifier.size(36.dp).clickable { @@ -155,7 +167,7 @@ fun ChatItemView( }, contentAlignment = Alignment.Center ) { - Text(r.text) + ReactionIcon(r.text, 12.sp) } } } @@ -164,6 +176,7 @@ fun ChatItemView( @Composable fun MsgContentItemDropdownMenu() { + val saveFileLauncher = rememberSaveFileLauncher(ciFile = cItem.file) DefaultDropdownMenu(showMenu) { if (cInfo.featureEnabled(ChatFeature.Reactions) && cItem.allowAddReaction) { MsgReactionsMenu() @@ -178,37 +191,21 @@ fun ChatItemView( showMenu.value = false }) } + val clipboard = LocalClipboardManager.current ItemAction(stringResource(MR.strings.share_verb), painterResource(MR.images.ic_share), onClick = { - val filePath = getLoadedFilePath(cItem.file) + val fileSource = getLoadedFileSource(cItem.file) when { - filePath != null -> shareFile(cItem.text, filePath) - else -> shareText(cItem.content.text) + fileSource != null -> shareFile(cItem.text, fileSource) + else -> clipboard.shareText(cItem.content.text) } showMenu.value = false }) ItemAction(stringResource(MR.strings.copy_verb), painterResource(MR.images.ic_content_copy), onClick = { - copyText(cItem.content.text) + clipboard.setText(AnnotatedString(cItem.content.text)) showMenu.value = false }) - if (cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) { - val filePath = getLoadedFilePath(cItem.file) - if (filePath != null) { - val writePermissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE) - ItemAction(stringResource(MR.strings.save_verb), painterResource(MR.images.ic_download), onClick = { - when (cItem.content.msgContent) { - is MsgContent.MCImage -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || writePermissionState.hasPermission) { - saveImage(cItem.file) - } else { - writePermissionState.launchPermissionRequest() - } - } - is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> saveFileLauncher.launch(cItem.file?.fileName) - else -> {} - } - showMenu.value = false - }) - } + if ((cItem.content.msgContent is MsgContent.MCImage || cItem.content.msgContent is MsgContent.MCVideo || cItem.content.msgContent is MsgContent.MCFile || cItem.content.msgContent is MsgContent.MCVoice) && getLoadedFilePath(cItem.file) != null) { + SaveContentItemAction(cItem, saveFileLauncher, showMenu) } if (cItem.meta.editable && cItem.content.msgContent !is MsgContent.MCVoice && !live) { ItemAction(stringResource(MR.strings.edit_verb), painterResource(MR.images.ic_edit_filled), onClick = { @@ -262,7 +259,7 @@ fun ChatItemView( fun ContentItem() { val mc = cItem.content.msgContent if (cItem.meta.itemDeleted != null && !revealed.value) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { @@ -281,7 +278,7 @@ fun ChatItemView( } @Composable fun DeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + DeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, showMenu, questionText = deleteMessageQuestionText(), deleteMessage) @@ -292,9 +289,48 @@ fun ChatItemView( CICallItemView(cInfo, cItem, status, duration, acceptCall) } + fun eventItemViewText(): AnnotatedString { + val memberDisplayName = cItem.memberDisplayName + return if (memberDisplayName != null) { + buildAnnotatedString { + withStyle(chatEventStyle) { append(memberDisplayName) } + append(" ") + }.plus(chatEventText(cItem)) + } else { + chatEventText(cItem) + } + } + + @Composable fun EventItemView() { + CIEventView(eventItemViewText()) + } + + fun membersConnectedText(): String? { + return if (getConnectedMemberNames != null) { + val ns = getConnectedMemberNames() + when { + ns.size > 3 -> String.format(generalGetString(MR.strings.rcv_group_event_n_members_connected), ns[0], ns[1], ns.size - 2) + ns.size == 3 -> String.format(generalGetString(MR.strings.rcv_group_event_3_members_connected), ns[0], ns[1], ns[2]) + ns.size == 2 -> String.format(generalGetString(MR.strings.rcv_group_event_2_members_connected), ns[0], ns[1]) + else -> null + } + } else { + null + } + } + + fun membersConnectedItemText(): AnnotatedString { + val t = membersConnectedText() + return if (t != null) { + chatEventText(t, cItem.timestampText) + } else { + eventItemViewText() + } + } + @Composable fun ModeratedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, showMember = showMember) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage) @@ -308,14 +344,22 @@ fun ChatItemView( is CIContent.RcvDeleted -> DeletedItem() is CIContent.SndCall -> CallItem(c.status, c.duration) is CIContent.RcvCall -> CallItem(c.status, c.duration) - is CIContent.RcvIntegrityError -> IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL, showMember = showMember) - is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, showMember = showMember) + is CIContent.RcvIntegrityError -> if (developerTools) { + IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL) + } else { + Box(Modifier.size(0.dp)) {} + } + is CIContent.RcvDecryptionError -> CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember) is CIContent.RcvGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito) - is CIContent.RcvGroupEventContent -> CIEventView(cItem) - is CIContent.SndGroupEventContent -> CIEventView(cItem) - is CIContent.RcvConnEventContent -> CIEventView(cItem) - is CIContent.SndConnEventContent -> CIEventView(cItem) + is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) { + is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText()) + is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat) + else -> EventItemView() + } + is CIContent.SndGroupEventContent -> EventItemView() + is CIContent.RcvConnEventContent -> EventItemView() + is CIContent.SndConnEventContent -> EventItemView() is CIContent.RcvChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.SndChatFeature -> CIChatFeatureView(cItem, c.feature, c.enabled.iconColor) is CIContent.RcvChatPreference -> { @@ -340,6 +384,12 @@ fun ChatItemView( } } +@Composable +expect fun ReactionIcon(text: String, fontSize: TextUnit = TextUnit.Unspecified) + +@Composable +expect fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) + @Composable fun CancelFileItemAction( fileId: Long, @@ -523,12 +573,13 @@ fun PreviewChatItemView() { linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, deleteMessage = { _, _ -> }, - receiveFile = {}, + receiveFile = { _, _ -> }, cancelFile = {}, joinGroup = {}, acceptCall = { _ -> }, scrollToItem = {}, acceptFeature = { _, _, _ -> }, + openDirectChat = { _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -537,6 +588,7 @@ fun PreviewChatItemView() { findModelMember = { null }, setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, + developerTools = false, ) } } @@ -552,12 +604,13 @@ fun PreviewChatItemViewDeletedContent() { linkMode = SimplexLinkMode.DESCRIPTION, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, deleteMessage = { _, _ -> }, - receiveFile = {}, + receiveFile = { _, _ -> }, cancelFile = {}, joinGroup = {}, acceptCall = { _ -> }, scrollToItem = {}, acceptFeature = { _, _, _ -> }, + openDirectChat = { _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -566,6 +619,7 @@ fun PreviewChatItemViewDeletedContent() { findModelMember = { null }, setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, + developerTools = false, ) } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt similarity index 77% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 49a9ce28e6..2d949e1737 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -10,14 +9,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.* +import chat.simplex.common.model.ChatItem +import chat.simplex.common.ui.theme.* @Composable -fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sent = ci.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -31,7 +30,6 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), @@ -42,11 +40,10 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" -) +)*/ @Composable fun PreviewDeletedItemView() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt similarity index 85% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt index 86fffc6ae0..3ede737ffa 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/EmojiItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -9,10 +9,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.ChatItem +import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.MREmojiChar +import chat.simplex.common.ui.theme.EmojiFont -val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp) -val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp) +val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFont) +val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont) @Composable fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) { @@ -26,10 +28,7 @@ fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) { } @Composable -fun EmojiText(text: String) { - val s = text.trim() - Text(s, style = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont) -} +expect fun EmojiText(text: String) // https://stackoverflow.com/a/46279500 private const val emojiStr = "^(" + diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt similarity index 93% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 6dc7920235..122e54c3b2 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item import androidx.compose.foundation.* import androidx.compose.foundation.layout.* @@ -10,7 +10,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.* import androidx.compose.ui.platform.UriHandler @@ -19,14 +18,14 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.unit.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.platform.base64ToBitmap +import chat.simplex.common.views.chat.MEMBER_IMAGE_SIZE import chat.simplex.res.MR -import kotlinx.datetime.Clock import kotlin.math.min @Composable @@ -35,10 +34,9 @@ fun FramedItemView( ci: ChatItem, uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, - showMember: Boolean = false, linkMode: SimplexLinkMode, showMenu: MutableState<Boolean>, - receiveFile: (Long) -> Unit, + receiveFile: (Long, Boolean) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, scrollToItem: (Long) -> Unit = {}, ) { @@ -52,17 +50,39 @@ fun FramedItemView( @Composable fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f) + @Composable + fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { + MarkdownText( + qi.text, + qi.formattedText, + maxLines = lines, + overflow = TextOverflow.Ellipsis, + style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), + linkMode = linkMode, + uriHandler = if (appPlatform.isDesktop) uriHandler else null + ) + } + @Composable fun ciQuotedMsgView(qi: CIQuote) { Box( Modifier.padding(vertical = 6.dp, horizontal = 12.dp), contentAlignment = Alignment.TopStart ) { - MarkdownText( - qi.text, qi.formattedText, sender = qi.sender(membership()), senderBold = true, maxLines = 3, - style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface), - linkMode = linkMode - ) + val sender = qi.sender(membership()) + if (sender != null) { + Column( + horizontalAlignment = Alignment.Start + ) { + Text( + sender, + style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + ) + ciQuotedMsgTextView(qi, lines = 2) + } + } else { + ciQuotedMsgTextView(qi, lines = 3) + } } } @@ -110,13 +130,14 @@ fun FramedItemView( onLongClick = { showMenu.value = true }, onClick = { scrollToItem(qi.itemId?: return@combinedClickable) } ) + .onRightClick { showMenu.value = true } ) { when (qi.content) { is MsgContent.MCImage -> { Box(Modifier.fillMaxWidth().weight(1f)) { ciQuotedMsgView(qi) } - val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap() + val imageBitmap = base64ToBitmap(qi.content.image) Image( imageBitmap, contentDescription = stringResource(MR.strings.image_descr), @@ -128,7 +149,7 @@ fun FramedItemView( Box(Modifier.fillMaxWidth().weight(1f)) { ciQuotedMsgView(qi) } - val imageBitmap = base64ToBitmap(qi.content.image).asImageBitmap() + val imageBitmap = base64ToBitmap(qi.content.image) Image( imageBitmap, contentDescription = stringResource(MR.strings.video_descr), @@ -158,7 +179,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, showMember, linkMode = linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) } } @@ -205,11 +226,11 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, ci.encryptLocalFile, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCVideo -> { @@ -217,27 +238,29 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCVoice -> { CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview) - CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + } } - else -> CIMarkdownText(ci, chatTTL, showMember, linkMode, uriHandler, onLinkLongClick) + else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) } } } @@ -253,7 +276,6 @@ fun FramedItemView( fun CIMarkdownText( ci: ChatItem, chatTTL: Int?, - showMember: Boolean, linkMode: SimplexLinkMode, uriHandler: UriHandler?, onLinkLongClick: (link: String) -> Unit = {} @@ -261,7 +283,7 @@ fun CIMarkdownText( Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( - text, if (text.isEmpty()) emptyList() else ci.formattedText, if (showMember) ci.memberDisplayName else null, + text, if (text.isEmpty()) emptyList() else ci.formattedText, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick ) @@ -283,8 +305,8 @@ fun PriorityLayout( content: @Composable () -> Unit ) { /** - * Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints] - * */ + * Limiting max value for height + width in order to not crash the app, see [androidx.compose.ui.unit.Constraints.createConstraints] + * */ fun maxSafeHeight(width: Int) = when { // width bits + height bits should be <= 31 width < 0x1FFF /*MaxNonFocusMask*/ -> 0x3FFFF - 1 /* MaxFocusMask */ // 13 bits width + 18 bits height width < 0x7FFF /*MinNonFocusMask*/ -> 0xFFFF - 1 /* MinFocusMask */ // 15 bits width + 16 bits height @@ -317,6 +339,7 @@ fun PriorityLayout( } } } +/* class EditedProvider: PreviewParameterProvider<Boolean> { override val values = listOf(false, true).asSequence() @@ -506,3 +529,4 @@ fun PreviewQuoteWithLongTextAndFile(@PreviewParameter(EditedProvider::class) edi ) } } +*/ diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt similarity index 58% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 9cfaa3481f..05d11208fb 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -1,43 +1,23 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.graphics.Bitmap -import android.net.Uri -import android.os.Build -import android.view.View -import androidx.activity.compose.BackHandler import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.material.* +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.input.pointer.* -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.* -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.* -import androidx.compose.ui.viewinterop.AndroidView -import androidx.core.view.isVisible -import chat.simplex.app.R -import chat.simplex.app.views.chat.ProviderMedia -import chat.simplex.app.views.helpers.* -import coil.ImageLoader -import coil.compose.rememberAsyncImagePainter -import coil.decode.GifDecoder -import coil.decode.ImageDecoderDecoder -import coil.request.ImageRequest -import coil.size.Size -import com.google.accompanist.pager.* -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout -import com.google.android.exoplayer2.ui.StyledPlayerView -import chat.simplex.res.MR +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.ProviderMedia +import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch +import java.net.URI import kotlin.math.absoluteValue interface ImageGalleryProvider { @@ -49,7 +29,6 @@ interface ImageGalleryProvider { fun onDismiss(index: Int) } -@OptIn(ExperimentalPagerApi::class) @Composable fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> Unit) { val provider = remember { imageProvider() } @@ -65,11 +44,13 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> } } val scope = rememberCoroutineScope() - val playersToRelease = rememberSaveable { mutableSetOf<Uri>() } + val playersToRelease = rememberSaveable { mutableSetOf<URI>() } DisposableEffectOnGone( - whenGone = { playersToRelease.forEach { VideoPlayer.release(it, true, true) } } + whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } ) - HorizontalPager(count = remember { provider.totalMediaSize }.value, state = pagerState) { index -> + + @Composable + fun Content(index: Int) { Column( Modifier .fillMaxSize() @@ -141,32 +122,14 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> ) } .fillMaxSize() + // LALAL + // https://github.com/JetBrains/compose-multiplatform/pull/2015/files#diff-841b3825c504584012e1d1c834d731bae794cce6acad425d81847c8bbbf239e0R24 if (media is ProviderMedia.Image) { - val (uri: Uri, imageBitmap: Bitmap) = media - // I'm making a new instance of imageLoader here because if I use one instance in multiple places - // after end of composition here a GIF from the first instance will be paused automatically which isn't what I want - val imageLoader = ImageLoader.Builder(LocalContext.current) - .components { - if (Build.VERSION.SDK_INT >= 28) { - add(ImageDecoderDecoder.Factory()) - } else { - add(GifDecoder.Factory()) - } - } - .build() - Image( - rememberAsyncImagePainter( - ImageRequest.Builder(LocalContext.current).data(data = uri).size(Size.ORIGINAL).build(), - placeholder = BitmapPainter(imageBitmap.asImageBitmap()), // show original image while it's still loading by coil - imageLoader = imageLoader - ), - contentDescription = stringResource(MR.strings.image_descr), - contentScale = ContentScale.Fit, - modifier = modifier, - ) + val (data: ByteArray, imageBitmap: ImageBitmap) = media + FullScreenImageView(modifier, data, imageBitmap) } else if (media is ProviderMedia.Video) { val preview = remember(media.uri.path) { base64ToBitmap(media.preview) } - VideoView(modifier, media.uri, preview, index == settledCurrentPage) + VideoView(modifier, media.uri, preview, index == settledCurrentPage, close) DisposableEffect(Unit) { onDispose { playersToRelease.add(media.uri) } } @@ -174,12 +137,19 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> } } } + if (appPlatform.isAndroid) { + HorizontalPager(pageCount = remember { provider.totalMediaSize }.value, state = pagerState) { index -> Content(index) } + } else { + Content(pagerState.currentPage) + } } @Composable -private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, currentPage: Boolean) { - val context = LocalContext.current - val player = remember(uri) { VideoPlayer.getOrCreate(uri, true, defaultPreview, 0L, true) } +expect fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) + +@Composable +private fun VideoView(modifier: Modifier, uri: URI, defaultPreview: ImageBitmap, currentPage: Boolean, close: () -> Unit) { + val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, 0L, true) } val isCurrentPage = rememberUpdatedState(currentPage) val play = { player.play(true) @@ -191,29 +161,16 @@ private fun VideoView(modifier: Modifier, uri: Uri, defaultPreview: Bitmap, curr player.enableSound(true) snapshotFlow { isCurrentPage.value } .distinctUntilChanged() - .collect { if (it) play() else stop() } + .collect { + // Do not autoplay on desktop because it needs workaround + if (it && appPlatform.isAndroid) play() else if (!it) stop() + } } Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - AndroidView( - factory = { ctx -> - StyledPlayerView(ctx).apply { - resizeMode = if (ctx.resources.configuration.screenWidthDp > ctx.resources.configuration.screenHeightDp) { - AspectRatioFrameLayout.RESIZE_MODE_FIXED_HEIGHT - } else { - AspectRatioFrameLayout.RESIZE_MODE_FIXED_WIDTH - } - setShowPreviousButton(false) - setShowNextButton(false) - setShowSubtitleButton(false) - setShowVrButton(false) - controllerAutoShow = false - findViewById<View>(com.google.android.exoplayer2.R.id.exo_controls_background).setBackgroundColor(Color.Black.copy(alpha = 0.3f).toArgb()) - findViewById<View>(com.google.android.exoplayer2.R.id.exo_settings).isVisible = false - this.player = player.player - } - }, - modifier - ) + FullScreenVideoView(player, modifier, close) } } + +@Composable +expect fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt similarity index 77% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index 77a0219ca4..582730b8f5 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -13,21 +12,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.ChatItem -import chat.simplex.app.model.MsgErrorType -import chat.simplex.app.ui.theme.CurrentColors -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.AlertManager -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.MsgErrorType +import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable -fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { - CIMsgError(ci, timedMessagesTTL, showMember) { +fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) { + CIMsgError(ci, timedMessagesTTL) { when (msgError) { is MsgErrorType.MsgSkipped -> AlertManager.shared.showAlertMsg( @@ -52,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT } @Composable -fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false, onClick: () -> Unit) { +fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), @@ -65,7 +63,6 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false ) { Text( buildAnnotatedString { - appendSender(this, if (showMember) ci.memberDisplayName else null, true) withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), @@ -76,11 +73,10 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" -) +)*/ @Composable fun IntegrityErrorItemViewPreview() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt similarity index 78% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 22192a3692..84675a09b4 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -1,6 +1,4 @@ -package chat.simplex.app.views.chat.item - -import android.content.res.Configuration +package chat.simplex.common.views.chat.item import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -11,19 +9,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.CIDeleted -import chat.simplex.app.model.ChatItem -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.model.CIDeleted +import chat.simplex.common.model.ChatItem +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Boolean = false) { +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( @@ -50,7 +47,6 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showMember: Bool private fun MarkedDeletedText(text: String) { Text( buildAnnotatedString { - // appendSender(this, if (showMember) ci.memberDisplayName else null, true) // TODO font size withStyle(SpanStyle(fontSize = 12.sp, fontStyle = FontStyle.Italic, color = MaterialTheme.colors.secondary)) { append(text) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), @@ -60,11 +56,10 @@ private fun MarkedDeletedText(text: String) { ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode" -) +)*/ @Composable fun PreviewMarkedDeletedItemView() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt similarity index 73% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index de5fd1594e..eabab138ba 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -1,43 +1,32 @@ -package chat.simplex.app.views.chat.item +package chat.simplex.common.views.chat.item -import android.app.Activity -import android.content.ActivityNotFoundException -import android.util.Log -import androidx.annotation.IntRange -import androidx.compose.foundation.text.* +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.InlineTextContent import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.* -import androidx.core.text.BidiFormatter -import chat.simplex.app.TAG -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.CurrentColors -import chat.simplex.app.views.helpers.detectGesture +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 kotlinx.coroutines.* +import java.awt.* val reserveTimestampStyle = SpanStyle(color = Color.Transparent) val boldFont = SpanStyle(fontWeight = FontWeight.Medium) -fun appendGroupMember(b: AnnotatedString.Builder, chatItem: ChatItem, groupMemberBold: Boolean) { - if (chatItem.chatDir is CIDirection.GroupRcv) { - val name = chatItem.chatDir.groupMember.memberProfile.displayName - if (groupMemberBold) b.withStyle(boldFont) { append(name) } - else b.append(name) - b.append(": ") - } -} - fun appendSender(b: AnnotatedString.Builder, sender: String?, senderBold: Boolean) { if (sender != null) { if (senderBold) b.withStyle(boldFont) { append(sender) } @@ -56,7 +45,7 @@ private val typingIndicators: List<AnnotatedString> = listOf( ) -private fun typingIndicator(recent: Boolean, @IntRange (from = 0, to = 4) typingIdx: Int): AnnotatedString = buildAnnotatedString { +private fun typingIndicator(recent: Boolean, typingIdx: Int): AnnotatedString = buildAnnotatedString { pushStyle(SpanStyle(color = CurrentColors.value.colors.secondary, fontFamily = FontFamily.Monospace, letterSpacing = (-1).sp)) append(if (recent) typingIndicators[typingIdx] else noTyping) } @@ -82,12 +71,12 @@ fun MarkdownText ( onLinkLongClick: (link: String) -> Unit = {} ) { val textLayoutDirection = remember (text) { - if (BidiFormatter.getInstance().isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr + if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr } val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { "\n" } else if (meta != null) { - reserveSpaceForMeta(meta, chatTTL) + reserveSpaceForMeta(meta, chatTTL, null) // LALAL } else { " " } @@ -117,18 +106,14 @@ fun MarkdownText ( } } if (meta?.isLive == true) { - val activity = LocalContext.current as Activity LaunchedEffect(meta.recent, meta.isLive) { switchTyping() } - DisposableEffect(Unit) { - val orientation = activity.resources.configuration.orientation - onDispose { - if (orientation == activity.resources.configuration.orientation) { - stopTyping() - } + DisposableEffectOnGone( + whenGone = { + stopTyping() } - } + ) } if (formattedText == null) { val annotatedText = buildAnnotatedString { @@ -156,7 +141,7 @@ fun MarkdownText ( } else { ft.format.style } - withAnnotation(tag = "URL", annotation = link) { + withAnnotation(tag = if (ft.format is Format.SimplexLink && linkMode != SimplexLinkMode.BROWSER) "SIMPLEX_URL" else "URL", annotation = link) { withStyle(ftStyle) { append(ft.viewText(linkMode)) } } } else { @@ -173,25 +158,42 @@ fun MarkdownText ( else */if (meta != null) withStyle(reserveTimestampStyle) { append(reserve) } } if (hasLinks && uriHandler != null) { - ClickableText(annotatedText, style = style, modifier = modifier, maxLines = maxLines, overflow = overflow, + 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) } }, onClick = { offset -> annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) .firstOrNull()?.let { annotation -> try { uriHandler.openUri(annotation.item) - } catch (e: ActivityNotFoundException) { + } 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) + } + }, + onHover = { offset -> + icon.value = annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset) + .firstOrNull()?.let { + PointerIcon.Hand + } ?: annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset) + .firstOrNull()?.let { + PointerIcon.Hand + } ?: PointerIcon.Default }, shouldConsumeEvent = { offset -> annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset).any() + annotatedText.getStringAnnotations(tag = "SIMPLEX_URL", start = offset, end = offset).any() } ) } else { @@ -212,6 +214,7 @@ fun ClickableText( onTextLayout: (TextLayoutResult) -> Unit = {}, onClick: (Int) -> Unit, onLongClick: (Int) -> Unit = {}, + onHover: (Int) -> Unit = {}, shouldConsumeEvent: (Int) -> Boolean ) { val layoutResult = remember { mutableStateOf<TextLayoutResult?>(null) } @@ -228,13 +231,21 @@ fun ClickableText( } } }, shouldConsumeEvent = { pos -> - var consume = false - layoutResult.value?.let { layoutResult -> - consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos)) - } - consume + var consume = false + layoutResult.value?.let { layoutResult -> + consume = shouldConsumeEvent(layoutResult.getOffsetForPosition(pos)) } + consume + } ) + }.pointerInput(onHover) { + if (appPlatform.isDesktop) { + detectCursorMove { pos -> + layoutResult.value?.let { layoutResult -> + onHover(layoutResult.getOffsetForPosition(pos)) + } + } + } } BasicText( @@ -250,3 +261,13 @@ fun ClickableText( } ) } + +private fun isRtl(s: CharSequence): Boolean { + for (element in s) { + val d = Character.getDirectionality(element) + if (d == Character.DIRECTIONALITY_RIGHT_TO_LEFT || d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC || d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING || d == Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE) { + return true + } + } + return false +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt similarity index 84% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt index 94f9632080..866ad04b02 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatHelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatHelpView.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist -import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -11,15 +10,14 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.annotatedStringResource -import chat.simplex.app.views.onboarding.ReadableTextWithLink -import chat.simplex.app.views.usersettings.MarkdownHelpView -import chat.simplex.app.views.usersettings.simplexTeamUri +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.annotatedStringResource +import chat.simplex.common.views.onboarding.ReadableTextWithLink +import chat.simplex.common.views.usersettings.MarkdownHelpView +import chat.simplex.common.views.usersettings.simplexTeamUri import chat.simplex.res.MR val bold = SpanStyle(fontWeight = FontWeight.Bold) @@ -30,7 +28,7 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) { verticalArrangement = Arrangement.spacedBy(10.dp) ) { Text(stringResource(MR.strings.thank_you_for_installing_simplex), lineHeight = 22.sp) - ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri) + ReadableTextWithLink(MR.strings.you_can_connect_to_simplex_chat_founder, simplexTeamUri, simplexLink = true) Column( Modifier.padding(top = 24.dp), verticalArrangement = Arrangement.spacedBy(10.dp) @@ -76,12 +74,11 @@ fun ChatHelpView(addContact: (() -> Unit)? = null) { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatHelpLayout() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt similarity index 80% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index c923b2fe14..7a0fc12786 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -1,6 +1,6 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist -import android.content.res.Configuration +import SectionItemView import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -13,18 +13,21 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.* -import chat.simplex.app.views.chat.group.deleteGroupDialog -import chat.simplex.app.views.chat.group.leaveGroupDialog -import chat.simplex.app.views.chat.item.InvalidJSONView -import chat.simplex.app.views.chat.item.ItemAction -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.ContactConnectionInfoView +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.group.deleteGroupDialog +import chat.simplex.common.views.chat.group.leaveGroupDialog +import chat.simplex.common.views.chat.item.InvalidJSONView +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.datetime.Clock @@ -41,11 +44,12 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { showMenu.value = false delay(500L) } + val showChatPreviews = chatModel.showChatPreviews.value when (chat.chatInfo) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode) }, click = { directChatAction(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) }, showMenu, @@ -54,7 +58,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } is ChatInfo.Group -> ChatListNavLinkLayout( - chatLinkPreview = { ChatPreviewView(chat, chatModel.draft.value, chatModel.draftChatId.value, chatModel.incognito.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) }, + chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, stopped, linkMode) }, click = { groupChatAction(chat.chatInfo.groupInfo, chatModel) }, dropdownMenuItems = { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, showMarkRead) }, showMenu, @@ -62,7 +66,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ) is ChatInfo.ContactRequest -> ChatListNavLinkLayout( - chatLinkPreview = { ContactRequestView(chatModel.incognito.value, chat.chatInfo) }, + chatLinkPreview = { ContactRequestView(chat.chatInfo) }, click = { contactRequestAlertDialog(chat.chatInfo, chatModel) }, dropdownMenuItems = { ContactRequestMenuItems(chat.chatInfo, chatModel, showMenu) }, showMenu, @@ -72,7 +76,9 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { ChatListNavLinkLayout( chatLinkPreview = { ContactConnectionView(chat.chatInfo.contactConnection) }, click = { - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.center.closeModals() + ModalManager.end.closeModals() + ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> ContactConnectionInfoView(chatModel, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) } }, @@ -86,7 +92,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { InvalidDataView() }, click = { - ModalManager.shared.showModal(true) { InvalidJSONView(chat.chatInfo.json) } + ModalManager.end.closeModals() + ModalManager.center.showModal(true) { InvalidJSONView(chat.chatInfo.json) } }, dropdownMenuItems = null, showMenu, @@ -96,38 +103,47 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) { } fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) { - if (chatInfo.ready) { - withApi { openChat(chatInfo, chatModel) } - } else { - pendingContactAlertDialog(chatInfo, chatModel) - } + withBGApi { openChat(chatInfo, chatModel) } } fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(groupInfo, chatModel) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert() - else -> withApi { openChat(ChatInfo.Group(groupInfo), chatModel) } + else -> withBGApi { openChat(ChatInfo.Group(groupInfo), chatModel) } + } +} + +suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) { + val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId) + if (chat != null) { + openChat(chat, chatModel) } } suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId) if (chat != null) { - chatModel.chatItems.clear() - chatModel.chatItems.addAll(chat.chatItems) - chatModel.chatId.value = chatInfo.id + openChat(chat, chatModel) } } +suspend fun openChat(chat: Chat, chatModel: ChatModel) { + chatModel.chatItems.clear() + chatModel.chatItems.addAll(chat.chatItems) + chatModel.chatId.value = chat.chatInfo.id +} + suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) { val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT) val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return + if (chatModel.chatId.value != chat.id) return chatModel.chatItems.addAll(0, chat.chatItems) } suspend fun apiFindMessages(chatInfo: ChatInfo, chatModel: ChatModel, search: String) { val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, search = search) ?: return + if (chatModel.chatId.value != chat.id) return chatModel.chatItems.clear() chatModel.chatItems.addAll(0, chat.chatItems) } @@ -205,7 +221,7 @@ fun MarkReadChatAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState< painterResource(MR.images.ic_check), onClick = { markChatRead(chat, chatModel) - chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) + ntfManager.cancelNotificationsForChat(chat.id) showMenu.value = false } ) @@ -316,11 +332,20 @@ fun LeaveGroupAction(groupInfo: GroupInfo, chatModel: ChatModel, showMenu: Mutab @Composable fun ContactRequestMenuItems(chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState<Boolean>) { ItemAction( - if (chatModel.incognito.value) stringResource(MR.strings.accept_contact_incognito_button) else stringResource(MR.strings.accept_contact_button), - if (chatModel.incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_check), - color = if (chatModel.incognito.value) Indigo else MaterialTheme.colors.onBackground, + stringResource(MR.strings.accept_contact_button), + painterResource(MR.images.ic_check), + color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(incognito = false, chatInfo.apiId, chatInfo, true, chatModel) + showMenu.value = false + } + ) + ItemAction( + stringResource(MR.strings.accept_contact_incognito_button), + painterResource(MR.images.ic_theater_comedy), + color = MaterialTheme.colors.onBackground, + onClick = { + acceptContactRequest(incognito = true, chatInfo.apiId, chatInfo, true, chatModel) showMenu.value = false } ) @@ -341,7 +366,9 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: stringResource(MR.strings.set_contact_name), painterResource(MR.images.ic_edit), onClick = { - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.center.closeModals() + ModalManager.end.closeModals() + ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> ContactConnectionInfoView(chatModel, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close) } showMenu.value = false @@ -351,7 +378,12 @@ fun ContactConnectionMenuItems(chatInfo: ChatInfo.ContactConnection, chatModel: stringResource(MR.strings.delete_verb), painterResource(MR.images.ic_delete), onClick = { - deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) {} + deleteContactConnectionAlert(chatInfo.contactConnection, chatModel) { + if (chatModel.chatId.value == null) { + ModalManager.center.closeModals() + ModalManager.end.closeModals() + } + } showMenu.value = false }, color = Color.Red @@ -424,19 +456,37 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { } fun contactRequestAlertDialog(contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { - AlertManager.shared.showAlertDialog( + AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.accept_connection_request__question), - text = generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified), - confirmText = if (chatModel.incognito.value) generalGetString(MR.strings.accept_contact_incognito_button) else generalGetString(MR.strings.accept_contact_button), - onConfirm = { acceptContactRequest(contactRequest.apiId, contactRequest, true, chatModel) }, - dismissText = generalGetString(MR.strings.reject_contact_button), - onDismiss = { rejectContactRequest(contactRequest, chatModel) } + text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(incognito = false, contactRequest.apiId, contactRequest, true, chatModel) + }) { + Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(incognito = true, contactRequest.apiId, contactRequest, true, chatModel) + }) { + Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + rejectContactRequest(contactRequest, chatModel) + }) { + Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + } + } ) } -fun acceptContactRequest(apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { +fun acceptContactRequest(incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { withApi { - val contact = chatModel.controller.apiAcceptContactRequest(apiId) + val contact = chatModel.controller.apiAcceptContactRequest(incognito, apiId) if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(ChatInfo.Direct(contact), listOf()) chatModel.replaceChat(contactRequest.id, chat) @@ -451,38 +501,6 @@ fun rejectContactRequest(contactRequest: ChatInfo.ContactRequest, chatModel: Cha } } -fun contactConnectionAlertDialog(connection: PendingContactConnection, chatModel: ChatModel) { - AlertManager.shared.showAlertDialogButtons( - title = generalGetString( - if (connection.initiated) MR.strings.you_invited_your_contact - else MR.strings.you_accepted_connection - ), - text = generalGetString( - if (connection.viaContactUri) MR.strings.you_will_be_connected_when_your_connection_request_is_accepted - else MR.strings.you_will_be_connected_when_your_contacts_device_is_online - ), - buttons = { - Row( - Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 2.dp), - horizontalArrangement = Arrangement.Center, - ) { - TextButton(onClick = { - AlertManager.shared.hideAlert() - deleteContactConnectionAlert(connection, chatModel) {} - }) { - Text(stringResource(MR.strings.delete_verb)) - } - Spacer(Modifier.padding(horizontal = 4.dp)) - TextButton(onClick = { AlertManager.shared.hideAlert() }) { - Text(stringResource(MR.strings.ok)) - } - } - } - ) -} - fun deleteContactConnectionAlert(connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_pending_connection__question), @@ -514,7 +532,10 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) { val r = chatModel.controller.apiDeleteChat(chatInfo.chatType, chatInfo.apiId) if (r) { chatModel.removeChat(chatInfo.id) - chatModel.chatId.value = null + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } } } }, @@ -547,8 +568,11 @@ fun deleteGroup(groupInfo: GroupInfo, chatModel: ChatModel) { val r = chatModel.controller.apiDeleteChat(ChatType.Group, groupInfo.apiId) if (r) { chatModel.removeChat(groupInfo.id) - chatModel.chatId.value = null - chatModel.controller.ntfManager.cancelNotificationsForChat(groupInfo.id) + if (chatModel.chatId.value == groupInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(groupInfo.id) } } } @@ -593,7 +617,7 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo if (res && newChatInfo != null) { chatModel.updateChatInfo(newChatInfo) if (!chatSettings.enableNtfs) { - chatModel.controller.ntfManager.cancelNotificationsForChat(chat.id) + ntfManager.cancelNotificationsForChat(chat.id) } val current = currentState?.value if (current != null) { @@ -612,7 +636,9 @@ fun ChatListNavLinkLayout( stopped: Boolean ) { var modifier = Modifier.fillMaxWidth() - if (!stopped) modifier = modifier.combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) + if (!stopped) modifier = modifier + .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true } Box(modifier) { Row( modifier = Modifier @@ -629,12 +655,11 @@ fun ChatListNavLinkLayout( Divider(Modifier.padding(horizontal = 8.dp)) } -@Preview -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatListNavLinkDirect() { SimpleXTheme { @@ -653,9 +678,9 @@ fun PreviewChatListNavLinkDirect() { ), chatStats = Chat.ChatStats() ), + true, null, null, - false, null, null, stopped = false, @@ -670,12 +695,11 @@ fun PreviewChatListNavLinkDirect() { } } -@Preview -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatListNavLinkGroup() { SimpleXTheme { @@ -694,9 +718,9 @@ fun PreviewChatListNavLinkGroup() { ), chatStats = Chat.ChatStats() ), + true, null, null, - false, null, null, stopped = false, @@ -711,18 +735,17 @@ fun PreviewChatListNavLinkGroup() { } } -@Preview -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatListNavLinkContactRequest() { SimpleXTheme { ChatListNavLinkLayout( chatLinkPreview = { - ContactRequestView(false, ChatInfo.ContactRequest.sampleData) + ContactRequestView(ChatInfo.ContactRequest.sampleData) }, click = {}, dropdownMenuItems = null, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt similarity index 68% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 3d93cab587..6d7450a213 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -1,6 +1,6 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist -import androidx.activity.compose.BackHandler +import SectionItemView import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* @@ -9,34 +9,35 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* -import chat.simplex.app.* -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.NewChatSheet -import chat.simplex.app.views.onboarding.WhatsNewView -import chat.simplex.app.views.onboarding.shouldShowWhatsNew -import chat.simplex.app.views.usersettings.SettingsView -import chat.simplex.app.views.usersettings.simplexTeamUri +import chat.simplex.common.SettingsViewState +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.WhatsNewView +import chat.simplex.common.views.onboarding.shouldShowWhatsNew +import chat.simplex.common.views.usersettings.SettingsView +import chat.simplex.common.views.usersettings.simplexTeamUri +import chat.simplex.common.platform.* +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch +import java.net.URI @Composable -fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { +fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } - val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } val showNewChatSheet = { newChatSheetState.value = AnimatedViewState.VISIBLE } @@ -47,7 +48,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: LaunchedEffect(Unit) { if (shouldShowWhatsNew(chatModel)) { delay(1000L) - ModalManager.shared.showCustomModal { close -> WhatsNewView(close = close) } + ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) } } } LaunchedEffect(chatModel.clearOverlays.value) { @@ -60,14 +61,24 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: connectIfOpenedViaUri(url, chatModel) } } + if (appPlatform.isDesktop) { + KeyChangeEffect(chatModel.chatId.value) { + if (chatModel.chatId.value != null) { + ModalManager.end.closeModalsExceptFirst() + } + AudioPlayer.stop() + VideoPlayerHolder.stopAll() + } + } + val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp var searchInList by rememberSaveable { mutableStateOf("") } - val scaffoldState = rememberScaffoldState() val scope = rememberCoroutineScope() - val switchingUsers = rememberSaveable { mutableStateOf(false) } - Scaffold(topBar = { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } }, + val (userPickerState, scaffoldState, switchingUsers ) = settingsState + Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(chatModel, scaffoldState.drawerState, userPickerState, stopped) { searchInList = it.trim() } } }, scaffoldState = scaffoldState, - drawerContent = { SettingsView(chatModel, setPerformLA) }, + drawerContent = { SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) }, drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), + drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { if (searchInList.isEmpty()) { FloatingActionButton( @@ -76,7 +87,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() } }, - Modifier.padding(end = DEFAULT_PADDING - 16.dp, bottom = DEFAULT_PADDING - 16.dp), + Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, @@ -91,7 +102,7 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: } } ) { - Box(Modifier.padding(it)) { + Box(Modifier.padding(it).padding(end = endPadding)) { Column( modifier = Modifier .fillMaxSize() @@ -112,8 +123,10 @@ fun ChatListView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, stopped: if (searchInList.isEmpty()) { NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) } - UserPicker(chatModel, userPickerState, switchingUsers) { - scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + if (appPlatform.isAndroid) { + UserPicker(chatModel, userPickerState, switchingUsers) { + scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } + } } if (switchingUsers.value) { Box( @@ -130,7 +143,7 @@ private fun OnboardingButtons(openNewChatSheet: () -> Unit) { Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) { val uriHandler = LocalUriHandler.current ConnectButton(generalGetString(MR.strings.chat_with_developers)) { - uriHandler.openUriCatching(simplexTeamUri) + uriHandler.openVerifiedSimplexUri(simplexTeamUri) } Spacer(Modifier.height(DEFAULT_PADDING)) ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet) @@ -178,7 +191,7 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user if (chatModel.chats.size > 0) { barButtons.add { IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb).capitalize(Locale.current), tint = MaterialTheme.colors.primary) + Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) } } } @@ -221,14 +234,6 @@ private fun ChatListToolbar(chatModel: ChatModel, drawerState: DrawerState, user }, title = { Row(verticalAlignment = Alignment.CenterVertically) { - if (chatModel.incognito.value) { - Icon( - painterResource(MR.images.ic_theater_comedy_filled), - stringResource(MR.strings.incognito), - tint = Indigo, - modifier = Modifier.padding(10.dp).size(26.dp) - ) - } Text( stringResource(MR.strings.your_chats), color = MaterialTheme.colors.onBackground, @@ -279,7 +284,7 @@ private fun BoxScope.unreadBadge(text: String? = "") { @Composable private fun ToggleFilterButton() { - val pref = remember { SimplexApp.context.chatModel.controller.appPrefs.showUnreadAndFavorites } + val pref = remember { ChatController.appPrefs.showUnreadAndFavorites } IconButton(onClick = { pref.set(!pref.get()) }) { Icon( painterResource(MR.images.ic_filter_list), @@ -306,6 +311,55 @@ private fun ProgressIndicator() { ) } +fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) { + Log.d(TAG, "connectIfOpenedViaUri: opened via link") + if (chatModel.currentUser.value == null) { + chatModel.appOpenUrl.value = uri + } else { + withUriAction(uri) { linkType -> + val title = when (linkType) { + ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link) + ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link) + ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link) + } + AlertManager.shared.showAlertDialogButtonsColumn( + title = title, + text = if (linkType == ConnectionLinkType.GROUP) + AnnotatedString(generalGetString(MR.strings.you_will_join_group)) + else + AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + Log.d(TAG, "connectIfOpenedViaUri: connecting") + connectViaUri(chatModel, linkType, uri, incognito = false) + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + Log.d(TAG, "connectIfOpenedViaUri: connecting incognito") + connectViaUri(chatModel, linkType, uri, incognito = true) + } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } + } +} + private var lazyListState = 0 to 0 @Composable @@ -314,8 +368,12 @@ private fun ChatList(chatModel: ChatModel, search: String) { DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } - val showUnreadAndFavorites = remember { chatModel.controller.appPrefs.showUnreadAndFavorites.state }.value - val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search) } } + val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value + val allChats = remember { chatModel.chats } + // In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side + // which is related to [derivedStateOf]. Using safe alternative instead + // val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } } + val chats = filteredChats(showUnreadAndFavorites, search, allChats.toList()) LazyColumn( modifier = Modifier.fillMaxWidth(), listState @@ -331,13 +389,12 @@ private fun ChatList(chatModel: ChatModel, search: String) { } } -private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String): List<Chat> { - val chatModel = SimplexApp.context.chatModel +private fun filteredChats(showUnreadAndFavorites: Boolean, searchText: String, chats: List<Chat>): List<Chat> { val s = searchText.trim().lowercase() return if (s.isEmpty() && !showUnreadAndFavorites) - chatModel.chats + chats else { - chatModel.chats.filter { chat -> + chats.filter { chat -> when (val cInfo = chat.chatInfo) { is ChatInfo.Direct -> if (s.isEmpty()) { filtered(chat) @@ -364,4 +421,3 @@ private fun filtered(chat: Chat): Boolean = private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean = cInfo.chatViewName.lowercase().contains(s.lowercase()) - diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt similarity index 71% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index efa51a3790..780e3515df 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist -import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -17,24 +16,24 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.ComposePreview -import chat.simplex.app.views.chat.ComposeState -import chat.simplex.app.views.chat.item.MarkdownText -import chat.simplex.app.views.helpers.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ComposePreview +import chat.simplex.common.views.chat.ComposeState +import chat.simplex.common.views.chat.item.MarkdownText +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.model.GroupInfo import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @Composable fun ChatPreviewView( chat: Chat, + showChatPreviews: Boolean, chatModelDraft: ComposeState?, chatModelDraftChatId: ChatId?, - chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, contactNetworkStatus: NetworkStatus?, stopped: Boolean, @@ -139,44 +138,48 @@ fun ChatPreviewView( } @Composable - fun chatPreviewText(chatModelIncognito: Boolean) { + fun chatPreviewText() { val ci = chat.chatItems.lastOrNull() if (ci != null) { - val (text: CharSequence, inlineTextContent) = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } - ci.meta.itemDeleted == null -> ci.text to null - else -> generalGetString(MR.strings.marked_deleted_description) to null - } - val formattedText = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> null - ci.meta.itemDeleted == null -> ci.formattedText - else -> null - } - MarkdownText( - text, - formattedText, - sender = when { + if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) { + val (text: CharSequence, inlineTextContent) = when { + chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { messageDraft(chatModelDraft) } + ci.meta.itemDeleted == null -> ci.text to null + else -> generalGetString(MR.strings.marked_deleted_description) to null + } + val formattedText = when { chatModelDraftChatId == chat.id && chatModelDraft != null -> null - cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName + ci.meta.itemDeleted == null -> ci.formattedText else -> null - }, - linkMode = linkMode, - senderBold = true, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), - inlineContent = inlineTextContent, - modifier = Modifier.fillMaxWidth(), - ) + } + MarkdownText( + text, + formattedText, + sender = when { + chatModelDraftChatId == chat.id && chatModelDraft != null -> null + cInfo is ChatInfo.Group && !ci.chatDir.sent -> ci.memberDisplayName + else -> null + }, + linkMode = linkMode, + senderBold = true, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), + inlineContent = inlineTextContent, + modifier = Modifier.fillMaxWidth(), + ) + } } else { when (cInfo) { is ChatInfo.Direct -> - if (!cInfo.ready) { + if (cInfo.contact.nextSendGrpInv) { + Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary) + } else if (!cInfo.ready) { Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary) } is ChatInfo.Group -> when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(chatModelIncognito, currentUserProfileDisplayName, cInfo.groupInfo)) + GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) else -> {} } @@ -185,6 +188,37 @@ fun ChatPreviewView( } } + @Composable + fun chatStatusImage() { + if (cInfo is ChatInfo.Direct) { + val descr = contactNetworkStatus?.statusString + when (contactNetworkStatus) { + is NetworkStatus.Connected -> + IncognitoIcon(chat.chatInfo.incognito) + + is NetworkStatus.Error -> + Icon( + painterResource(MR.images.ic_error), + contentDescription = descr, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(19.dp) + ) + + else -> + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(15.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 1.5.dp + ) + } + } else { + IncognitoIcon(chat.chatInfo.incognito) + } + } + Row { Box(contentAlignment = Alignment.BottomEnd) { ChatInfoImage(cInfo, size = 72.dp) @@ -200,14 +234,14 @@ fun ChatPreviewView( chatPreviewTitle() val height = with(LocalDensity.current) { 46.sp.toDp() } Row(Modifier.heightIn(min = height)) { - chatPreviewText(chatModelIncognito) + chatPreviewText() } } - val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt) Box( contentAlignment = Alignment.TopEnd ) { + val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.updatedAt) Text( ts, color = MaterialTheme.colors.secondary, @@ -263,24 +297,33 @@ fun ChatPreviewView( ) } } - if (cInfo is ChatInfo.Direct) { - Box( - Modifier.padding(top = 52.dp), - contentAlignment = Alignment.Center - ) { - ChatStatusImage(contactNetworkStatus) - } + Box( + Modifier.padding(top = 50.dp), + contentAlignment = Alignment.Center + ) { + chatStatusImage() } } } } @Composable -private fun groupInvitationPreviewText(chatModelIncognito: Boolean, currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String { +fun IncognitoIcon(incognito: Boolean) { + if (incognito) { + Icon( + painterResource(MR.images.ic_theater_comedy), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(21.dp) + ) + } +} + +@Composable +private fun groupInvitationPreviewText(currentUserProfileDisplayName: String?, groupInfo: GroupInfo): String { return if (groupInfo.membership.memberIncognito) String.format(stringResource(MR.strings.group_preview_join_as), groupInfo.membership.memberProfile.displayName) - else if (chatModelIncognito) - String.format(stringResource(MR.strings.group_preview_join_as), currentUserProfileDisplayName ?: "") else stringResource(MR.strings.group_preview_you_are_invited) } @@ -290,37 +333,14 @@ fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) } -@Composable -fun ChatStatusImage(s: NetworkStatus?) { - val descr = s?.statusString - if (s is NetworkStatus.Error) { - Icon( - painterResource(MR.images.ic_error), - contentDescription = descr, - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .size(19.dp) - ) - } else if (s !is NetworkStatus.Connected) { - CircularProgressIndicator( - Modifier - .padding(horizontal = 2.dp) - .size(15.dp), - color = MaterialTheme.colors.secondary, - strokeWidth = 1.5.dp - ) - } -} - -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatPreviewView() { SimpleXTheme { - ChatPreviewView(Chat.sampleData, null, null, false, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) + ChatPreviewView(Chat.sampleData, true, null, null, "", contactNetworkStatus = NetworkStatus.Connected(), stopped = false, linkMode = SimplexLinkMode.DESCRIPTION) } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt similarity index 75% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt index 39c0b583a3..99d6c5db15 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ContactConnectionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme @@ -7,14 +7,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity -import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ProfileImage +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.ProfileImage +import chat.simplex.common.model.PendingContactConnection +import chat.simplex.common.model.getTimestampText import chat.simplex.res.MR @Composable @@ -39,16 +39,22 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) { val height = with(LocalDensity.current) { 46.sp.toDp() } Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) } - val ts = getTimestampText(contactConnection.updatedAt) - Column( - Modifier.fillMaxHeight(), + Box( + contentAlignment = Alignment.TopEnd ) { + val ts = getTimestampText(contactConnection.updatedAt) Text( ts, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, modifier = Modifier.padding(bottom = 5.dp) ) + Box( + Modifier.padding(top = 50.dp), + contentAlignment = Alignment.Center + ) { + IncognitoIcon(contactConnection.incognito) + } } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt similarity index 80% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt index 8d95226730..8debcce98c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ContactRequestView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme @@ -11,14 +11,14 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ChatInfoImage +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.ChatInfoImage +import chat.simplex.common.model.ChatInfo +import chat.simplex.common.model.getTimestampText import chat.simplex.res.MR @Composable -fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.ContactRequest) { +fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { Row { ChatInfoImage(contactRequest, size = 72.dp) Column( @@ -32,7 +32,7 @@ fun ContactRequestView(chatModelIncognito: Boolean, contactRequest: ChatInfo.Con overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, - color = if (chatModelIncognito) Indigo else MaterialTheme.colors.primary + color = MaterialTheme.colors.primary ) val height = with(LocalDensity.current) { 46.sp.toDp() } Text(stringResource(MR.strings.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ShareListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt similarity index 91% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ShareListNavLinkView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt index 5156144854..b9b8ea54bc 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ShareListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist import SectionItemView import androidx.compose.foundation.layout.* @@ -9,9 +9,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.Indigo -import chat.simplex.app.views.helpers.ProfileImage +import chat.simplex.common.ui.theme.Indigo +import chat.simplex.common.views.helpers.ProfileImage +import chat.simplex.common.model.* @Composable fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt similarity index 79% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index 3a3f929fcc..8b65b2b5bc 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -1,7 +1,5 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -13,23 +11,26 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.SettingsViewState +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.Chat +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.BackHandler +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR import kotlinx.coroutines.flow.MutableStateFlow @Composable -fun ShareListView(chatModel: ChatModel, stopped: Boolean) { +fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stopped: Boolean) { var searchInList by rememberSaveable { mutableStateOf("") } - val userPickerState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } - val switchingUsers = rememberSaveable { mutableStateOf(false) } + val (userPickerState, scaffoldState, switchingUsers) = settingsState + val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp Scaffold( + Modifier.padding(end = endPadding), + scaffoldState = scaffoldState, topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, ) { Box(Modifier.padding(it)) { @@ -45,9 +46,11 @@ fun ShareListView(chatModel: ChatModel, stopped: Boolean) { } } } - UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = { - chatModel.sharedContent.value = null - }) + if (appPlatform.isAndroid) { + UserPicker(chatModel, userPickerState, switchingUsers, showSettings = false, showCancel = true, cancelClicked = { + chatModel.sharedContent.value = null + }) + } } @Composable @@ -83,7 +86,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState if (chatModel.chats.size >= 8) { barButtons.add { IconButton({ showSearch = true }) { - Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb).capitalize(Locale.current), tint = MaterialTheme.colors.primary) + Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) } } } @@ -118,14 +121,6 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.SemiBold, ) - if (chatModel.incognito.value) { - Icon( - painterResource(MR.images.ic_theater_comedy_filled), - stringResource(MR.strings.incognito), - tint = Indigo, - modifier = Modifier.padding(10.dp).size(26.dp) - ) - } } }, onTitleClick = null, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 5395c73f37..8c7dc2c605 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views.chatlist +package chat.simplex.common.views.chatlist import SectionItemView -import android.util.Log import androidx.compose.animation.core.* import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource @@ -13,19 +12,17 @@ import androidx.compose.ui.* import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.User +import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -44,6 +41,11 @@ fun UserPicker( ) { val scope = rememberCoroutineScope() var newChat by remember { mutableStateOf(userPickerState.value) } + if (newChat.isVisible()) { + BackHandler { + userPickerState.value = AnimatedViewState.HIDING + } + } val users by remember { derivedStateOf { chatModel.users @@ -91,10 +93,10 @@ fun UserPicker( } } val xOffset = with(LocalDensity.current) { 10.dp.roundToPx() } - val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density } + val maxWidth = with(LocalDensity.current) { windowWidth() * density } Box(Modifier .fillMaxSize() - .offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else xOffset, 0) } + .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else xOffset, 0) } .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { userPickerState.value = AnimatedViewState.HIDING }) .padding(bottom = 10.dp, top = 10.dp) .graphicsLayer { @@ -124,6 +126,7 @@ fun UserPicker( delay(500) switchingUsers.value = true } + ModalManager.closeAllModalsEverywhere() chatModel.controller.changeActiveUser(u.user.userId, null) job.cancel() switchingUsers.value = false @@ -162,15 +165,16 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues interactionSource = remember { MutableInteractionSource() }, indication = if (!u.activeUser) LocalIndication.current else null ) + .onRightClick { onLongClick() } .padding(padding), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { UserProfileRow(u) if (u.activeUser) { - Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) } else if (u.hidden) { - Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) + Icon(painterResource(MR.images.ic_lock), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary) } else if (unreadCount > 0) { Box( contentAlignment = Alignment.Center @@ -197,7 +201,7 @@ fun UserProfilePickerItem(u: User, unreadCount: Int = 0, padding: PaddingValues fun UserProfileRow(u: User) { Row( Modifier - .widthIn(max = LocalConfiguration.current.screenWidthDp.dp * 0.7f) + .widthIn(max = windowWidth() * 0.7f) .padding(vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt similarity index 56% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt index 69b8f49d1a..8738db2df1 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/ChatArchiveView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/ChatArchiveView.kt @@ -1,47 +1,40 @@ -package chat.simplex.app.views.database +package chat.simplex.common.views.database import SectionBottomSpacer import SectionTextFooter import SectionView -import android.content.Context -import android.content.res.Configuration -import android.net.Uri -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.datetime.* -import java.io.BufferedOutputStream import java.io.File +import java.net.URI import java.text.SimpleDateFormat import java.util.* @Composable fun ChatArchiveView(m: ChatModel, title: String, archiveName: String, archiveTime: Instant) { - val context = LocalContext.current - val archivePath = "${getFilesDirectory()}/$archiveName" - val saveArchiveLauncher = rememberSaveArchiveLauncher(archivePath) + val archivePath = filesDir.absolutePath + File.separator + archiveName + val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> + if (to != null) { + copyFileToFile(File(archivePath), to) {} + } + } ChatArchiveLayout( title, archiveTime, - saveArchive = { saveArchiveLauncher.launch(archivePath.substringAfterLast("/")) }, + saveArchive = { withApi { saveArchiveLauncher.launch(archivePath.substringAfterLast(File.separator)) }}, deleteArchiveAlert = { deleteArchiveAlert(m, archivePath) } ) } @@ -81,29 +74,6 @@ fun ChatArchiveLayout( } } -@Composable -private fun rememberSaveArchiveLauncher(chatArchivePath: String): ManagedActivityResultLauncher<String, Uri?> = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument(), - onResult = { destination -> - val cxt = SimplexApp.context - try { - destination?.let { - val contentResolver = cxt.contentResolver - contentResolver.openOutputStream(destination)?.let { stream -> - val outputStream = BufferedOutputStream(stream) - File(chatArchivePath).inputStream().use { it.copyTo(outputStream) } - outputStream.close() - Toast.makeText(cxt, generalGetString(MR.strings.file_saved), Toast.LENGTH_SHORT).show() - } - } - } catch (e: Error) { - Toast.makeText(cxt, generalGetString(MR.strings.error_saving_file), Toast.LENGTH_SHORT).show() - Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e") - } - } - ) - private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_chat_archive_question), @@ -113,7 +83,7 @@ private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { if (fileDeleted) { m.controller.appPrefs.chatArchiveName.set(null) m.controller.appPrefs.chatArchiveTime.set(null) - ModalManager.shared.closeModal() + ModalManager.start.closeModal() } else { Log.e(TAG, "deleteArchiveAlert delete() error") } @@ -122,12 +92,11 @@ private fun deleteArchiveAlert(m: ChatModel, archivePath: String) { ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewChatArchiveLayout() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt similarity index 71% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index 07be9dcef7..e34f80a7ef 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.database +package chat.simplex.common.views.database import SectionBottomSpacer import SectionItemView @@ -23,13 +23,14 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.* -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.datetime.Clock @@ -61,46 +62,8 @@ fun DatabaseEncryptionView(m: ChatModel) { initialRandomDBPassphrase, progressIndicator, onConfirmEncrypt = { - progressIndicator.value = true withApi { - try { - prefs.encryptionStartedAt.set(Clock.System.now()) - val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) - prefs.encryptionStartedAt.set(null) - val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError - when { - sqliteError is SQLiteError.ErrorNotADatabase -> { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.wrong_passphrase_title), - generalGetString(MR.strings.enter_correct_current_passphrase) - ) - } - } - error != null -> { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), - "failed to set storage encryption: ${error.responseType} ${error.details}" - ) - } - } - else -> { - prefs.initialRandomDBPassphrase.set(false) - initialRandomDBPassphrase.value = false - if (useKeychain.value) { - DatabaseUtils.ksDatabasePassword.set(newKey.value) - } - resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) - } - } - } - } catch (e: Exception) { - operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString()) - } - } + encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator) } } ) @@ -143,17 +106,11 @@ fun DatabaseEncryptionLayout( if (checked) { setUseKeychain(true, useKeychain, prefs) } else if (storedKey.value) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.remove_passphrase_from_keychain), - text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(), - confirmText = generalGetString(MR.strings.remove_passphrase), - onConfirm = { - DatabaseUtils.ksDatabasePassword.remove() - setUseKeychain(false, useKeychain, prefs) - storedKey.value = false - }, - destructive = true, - ) + removePassphraseAlert { + DatabaseUtils.ksDatabasePassword.remove() + setUseKeychain(false, useKeychain, prefs) + storedKey.value = false + } } else { setUseKeychain(false, useKeychain, prefs) } @@ -217,37 +174,13 @@ fun DatabaseEncryptionLayout( } Column { - if (chatDbEncrypted == false) { - SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted)) - } else if (useKeychain.value) { - if (storedKey.value) { - SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely)) - if (initialRandomDBPassphrase.value) { - SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase)) - } else { - SectionTextFooter(generalGetString(MR.strings.impossible_to_recover_passphrase)) - } - } else { - SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs)) - } - } else { - SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) - SectionTextFooter(generalGetString(MR.strings.impossible_to_recover_passphrase)) - } + DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase) } SectionBottomSpacer() } } -fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.encrypt_database_question), - text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(), - confirmText = generalGetString(MR.strings.encrypt_database), - onConfirm = onConfirm, - destructive = true, - ) -} +expect fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) fun encryptDatabaseAlert(onConfirm: () -> Unit) { AlertManager.shared.showAlertDialog( @@ -259,15 +192,7 @@ fun encryptDatabaseAlert(onConfirm: () -> Unit) { ) } -fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.change_database_passphrase_question), - text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(), - confirmText = generalGetString(MR.strings.update_database), - onConfirm = onConfirm, - destructive = false, - ) -} +expect fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) fun changeDatabaseKeyAlert(onConfirm: () -> Unit) { AlertManager.shared.showAlertDialog( @@ -279,37 +204,25 @@ fun changeDatabaseKeyAlert(onConfirm: () -> Unit) { ) } +expect fun removePassphraseAlert(onConfirm: () -> Unit) + @Composable -fun SavePassphraseSetting( +expect fun SavePassphraseSetting( useKeychain: Boolean, initialRandomDBPassphrase: Boolean, storedKey: Boolean, progressIndicator: Boolean, minHeight: Dp = TextFieldDefaults.MinHeight, onCheckedChange: (Boolean) -> Unit, -) { - SectionItemView(minHeight = minHeight) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled), - stringResource(MR.strings.save_passphrase_in_keychain), - tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary - ) - Spacer(Modifier.padding(horizontal = 4.dp)) - Text( - stringResource(MR.strings.save_passphrase_in_keychain), - Modifier.padding(end = 24.dp), - color = Color.Unspecified - ) - Spacer(Modifier.fillMaxWidth().weight(1f)) - DefaultSwitch( - checked = useKeychain, - onCheckedChange = onCheckedChange, - enabled = !initialRandomDBPassphrase && !progressIndicator - ) - } - } -} +) + +@Composable +expect fun DatabaseEncryptionFooter( + useKeychain: MutableState<Boolean>, + chatDbEncrypted: Boolean?, + storedKey: MutableState<Boolean>, + initialRandomDBPassphrase: MutableState<Boolean>, +) fun resetFormAfterEncryption( m: ChatModel, @@ -443,6 +356,62 @@ fun PassphraseField( } } +suspend fun encryptDatabase( + currentKey: MutableState<String>, + newKey: MutableState<String>, + confirmNewKey: MutableState<String>, + initialRandomDBPassphrase: MutableState<Boolean>, + useKeychain: MutableState<Boolean>, + storedKey: MutableState<Boolean>, + progressIndicator: MutableState<Boolean> +): Boolean { + val m = ChatModel + val prefs = ChatController.appPrefs + progressIndicator.value = true + return try { + prefs.encryptionStartedAt.set(Clock.System.now()) + val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) + prefs.encryptionStartedAt.set(null) + val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError + when { + sqliteError is SQLiteError.ErrorNotADatabase -> { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.wrong_passphrase_title), + generalGetString(MR.strings.enter_correct_current_passphrase) + ) + } + false + } + error != null -> { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), + "failed to set storage encryption: ${error.responseType} ${error.details}" + ) + } + false + } + else -> { + prefs.initialRandomDBPassphrase.set(false) + initialRandomDBPassphrase.value = false + if (useKeychain.value) { + DatabaseUtils.ksDatabasePassword.set(newKey.value) + } + resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) + } + true + } + } + } catch (e: Exception) { + operationEnded(m, progressIndicator) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString()) + } + false + } +} + // based on https://generatepasswords.org/how-to-calculate-entropy/ private fun passphraseEntropy(s: String): Double { var hasDigits = false diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index cf97b3c8c4..bce8fdf4f1 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -1,10 +1,9 @@ -package chat.simplex.app.views.database +package chat.simplex.common.views.database import SectionBottomSpacer import SectionSpacer import SectionView -import android.content.Context -import android.util.Log +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions @@ -13,17 +12,16 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.AppPreferences -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.AppVersionText -import chat.simplex.app.views.usersettings.NotificationsMode +import chat.simplex.common.model.AppPreferences +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.AppVersionText import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* @@ -42,7 +40,6 @@ fun DatabaseErrorView( val dbKey = remember { mutableStateOf("") } var storedDBKey by remember { mutableStateOf(DatabaseUtils.ksDatabasePassword.get()) } var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) } - val context = LocalContext.current val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences)) } fun callRunChat(confirmMigrations: MigrationConfirmation? = null) { @@ -199,18 +196,14 @@ private fun runChat( if (progressIndicator.value) return@launch progressIndicator.value = true try { - SimplexApp.context.initChatController(dbKey, confirmMigrations) + initChatController(dbKey, confirmMigrations) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } progressIndicator.value = false when (val status = chatDbStatus.value) { is DBMigrationResult.OK -> { - SimplexService.cancelPassphraseNotification() - when (prefs.notificationsMode.get()) { - NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start() } - NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() - } + platform.androidChatStartedAfterBeingOff() } is DBMigrationResult.ErrorNotADatabase -> AlertManager.shared.showAlertMsg(generalGetString(MR.strings.wrong_passphrase_title), generalGetString(MR.strings.enter_correct_passphrase)) @@ -228,12 +221,11 @@ private fun runChat( } private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean { - val context = SimplexApp.context val startedAt = prefs.encryptionStartedAt.get() ?: return false /** Just in case there is any small difference between reported Java's [Clock.System.now] and Linux's time on a file */ val safeDiffInTime = 10_000L - val filesChat = File(context.dataDir.absolutePath + File.separator + "files_chat.db.bak") - val filesAgent = File(context.dataDir.absolutePath + File.separator + "files_agent.db.bak") + val filesChat = File(dataDir.absolutePath + File.separator + "${chatDatabaseFileName}.bak") + val filesAgent = File(dataDir.absolutePath + File.separator + "${agentDatabaseFileName}.bak") return filesChat.exists() && filesAgent.exists() && startedAt.toEpochMilliseconds() - safeDiffInTime <= filesChat.lastModified() && @@ -241,9 +233,8 @@ private fun shouldShowRestoreDbButton(prefs: AppPreferences): Boolean { } private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPreferences) { - val context = SimplexApp.context - val filesChatBase = context.dataDir.absolutePath + File.separator + "files_chat.db" - val filesAgentBase = context.dataDir.absolutePath + File.separator + "files_agent.db" + val filesChatBase = dataDir.absolutePath + File.separator + chatDatabaseFileName + val filesAgentBase = dataDir.absolutePath + File.separator + agentDatabaseFileName try { Files.copy(Path("$filesChatBase.bak"), Path(filesChatBase), StandardCopyOption.REPLACE_EXISTING) Files.copy(Path("$filesAgentBase.bak"), Path(filesAgentBase), StandardCopyOption.REPLACE_EXISTING) @@ -264,6 +255,11 @@ private fun mtrErrorDescription(err: MTRError): String = @Composable private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + delay(100L) + focusRequester.requestFocus() + } PassphraseField( text, generalGetString(MR.strings.enter_passphrase), @@ -271,7 +267,15 @@ private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onCli keyboardActions = KeyboardActions(onDone = if (enabled) { { onClick?.invoke() } } else null - ) + ), + modifier = Modifier.focusRequester(focusRequester).onPreviewKeyEvent { + if (onClick != null && it.key == Key.Enter && it.type == KeyEventType.KeyUp) { + onClick() + true + } else { + false + } + } ) } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt similarity index 79% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 3fc2622033..fa0f8f54d1 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -1,18 +1,11 @@ -package chat.simplex.app.views.database +package chat.simplex.common.views.database import SectionBottomSpacer import SectionDividerSpaced import SectionTextFooter import SectionItemView import SectionView -import android.content.Context -import android.content.res.Configuration -import android.net.Uri -import android.util.Log -import android.widget.Toast -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -21,25 +14,21 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.fragment.app.FragmentActivity -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.* import kotlinx.datetime.* -import org.apache.commons.io.IOUtils import java.io.* +import java.net.URI +import java.nio.file.Files import java.text.SimpleDateFormat import java.util.* import kotlin.collections.ArrayList @@ -49,24 +38,26 @@ fun DatabaseView( m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) ) { - val context = LocalContext.current val progressIndicator = remember { mutableStateOf(false) } - val runChat = remember { m.chatRunning } val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val chatArchiveName = remember { mutableStateOf(prefs.chatArchiveName.get()) } val chatArchiveTime = remember { mutableStateOf(prefs.chatArchiveTime.get()) } val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } val chatArchiveFile = remember { mutableStateOf<String?>(null) } - val saveArchiveLauncher = rememberSaveArchiveLauncher(chatArchiveFile) - val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(getAppFilesDirectory())) } - val importArchiveLauncher = rememberGetContentLauncher { uri: Uri? -> - if (uri != null) { - importArchiveAlert(m, uri, appFilesCountAndSize, progressIndicator) + val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val file = chatArchiveFile.value + if (file != null && to != null) { + copyFileToFile(File(file), to) { + chatArchiveFile.value = null + } } } - LaunchedEffect(m.chatRunning) { - runChat.value = m.chatRunning.value ?: true + val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } + val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + importArchiveAlert(m, to, appFilesCountAndSize, progressIndicator) + } } val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) } Box( @@ -74,11 +65,13 @@ fun DatabaseView( ) { DatabaseLayout( progressIndicator.value, - runChat.value != false, + remember { m.chatRunning }.value != false, m.chatDbChanged.value, useKeychain.value, m.chatDbEncrypted.value, + m.controller.appPrefs.storeDBPassphrase.state.value, m.controller.appPrefs.initialRandomDBPassphrase, + m.controller.appPrefs.developerTools.state.value, importArchiveLauncher, chatArchiveName, chatArchiveTime, @@ -87,8 +80,8 @@ fun DatabaseView( chatItemTTL, m.currentUser.value, m.users, - startChat = { startChat(m, runChat, chatLastStart, m.chatDbChanged) }, - stopChatAlert = { stopChatAlert(m, runChat) }, + startChat = { startChat(m, chatLastStart, m.chatDbChanged) }, + stopChatAlert = { stopChatAlert(m) }, exportArchive = { exportArchive(m, progressIndicator, chatArchiveName, chatArchiveTime, chatArchiveFile, saveArchiveLauncher) }, deleteChatAlert = { deleteChatAlert(m, progressIndicator) }, deleteAppFilesAndMedia = { deleteFilesAndMediaAlert(appFilesCountAndSize) }, @@ -127,8 +120,10 @@ fun DatabaseLayout( chatDbChanged: Boolean, useKeyChain: Boolean, chatDbEncrypted: Boolean?, + passphraseSaved: Boolean, initialRandomDBPassphrase: SharedPreference<Boolean>, - importArchiveLauncher: ManagedActivityResultLauncher<String, Uri?>, + developerTools: Boolean, + importArchiveLauncher: FileChooserLauncher, chatArchiveName: MutableState<String?>, chatArchiveTime: MutableState<Instant?>, chatLastStart: MutableState<Instant?>, @@ -183,13 +178,21 @@ fun DatabaseLayout( SectionView(stringResource(MR.strings.chat_database_section)) { val unencrypted = chatDbEncrypted == false SettingsActionItem( - if (unencrypted) painterResource(MR.images.ic_lock_open) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) + if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_lock), stringResource(MR.strings.database_passphrase), click = showSettingsModal() { DatabaseEncryptionView(it) }, - iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary, + iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) + if (appPlatform.isDesktop && developerTools) { + SettingsActionItem( + painterResource(MR.images.ic_folder_open), + stringResource(MR.strings.open_database_folder), + ::desktopOpenDatabaseDir, + disabled = operationsDisabled + ) + } SettingsActionItem( painterResource(MR.images.ic_ios_share), stringResource(MR.strings.export_database), @@ -207,7 +210,7 @@ fun DatabaseLayout( SettingsActionItem( painterResource(MR.images.ic_download), stringResource(MR.strings.import_database), - { importArchiveLauncher.launch("application/zip") }, + { withApi { importArchiveLauncher.launch("application/zip") }}, textColor = Color.Red, iconColor = Color.Red, disabled = operationsDisabled @@ -332,47 +335,43 @@ fun chatArchiveTitle(chatArchiveTime: Instant, chatLastStart: Instant): String { return stringResource(if (chatArchiveTime < chatLastStart) MR.strings.old_database_archive else MR.strings.new_database_archive) } -private fun startChat(m: ChatModel, runChat: MutableState<Boolean?>, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) { +private fun startChat(m: ChatModel, chatLastStart: MutableState<Instant?>, chatDbChanged: MutableState<Boolean>) { withApi { try { if (chatDbChanged.value) { - SimplexApp.context.initChatController() + initChatController() chatDbChanged.value = false } if (m.chatDbStatus.value !is DBMigrationResult.OK) { /** Hide current view and show [DatabaseErrorView] */ - ModalManager.shared.closeModals() + ModalManager.closeAllModalsEverywhere() return@withApi } if (m.currentUser.value == null) { - ModalManager.shared.closeModals() + ModalManager.closeAllModalsEverywhere() return@withApi } else { m.controller.apiStartChat() - runChat.value = true m.chatRunning.value = true } val ts = Clock.System.now() m.controller.appPrefs.chatLastStart.set(ts) chatLastStart.value = ts - when (m.controller.appPrefs.notificationsMode.get()) { - NotificationsMode.SERVICE.name -> CoroutineScope(Dispatchers.Default).launch { SimplexService.start() } - NotificationsMode.PERIODIC.name -> SimplexApp.context.schedulePeriodicWakeUp() - } + platform.androidChatStartedAfterBeingOff() } catch (e: Error) { - runChat.value = false + m.chatRunning.value = false AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.toString()) } } } -private fun stopChatAlert(m: ChatModel, runChat: MutableState<Boolean?>) { +private fun stopChatAlert(m: ChatModel) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.stop_chat_question), text = generalGetString(MR.strings.stop_chat_to_export_import_or_delete_chat_database), confirmText = generalGetString(MR.strings.stop_chat_confirmation), - onConfirm = { authStopChat(m, runChat) }, - onDismiss = { runChat.value = true } + onConfirm = { authStopChat(m) }, + onDismiss = { m.chatRunning.value = true } ) } @@ -383,7 +382,7 @@ private fun exportProhibitedAlert() { ) } -private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>) { +private fun authStopChat(m: ChatModel) { if (m.controller.appPrefs.performLA.get()) { authenticate( generalGetString(MR.strings.auth_stop_chat), @@ -391,31 +390,29 @@ private fun authStopChat(m: ChatModel, runChat: MutableState<Boolean?>) { completed = { laResult -> when (laResult) { LAResult.Success, is LAResult.Unavailable -> { - stopChat(m, runChat) + stopChat(m) } is LAResult.Error -> { - runChat.value = true + m.chatRunning.value = true } is LAResult.Failed -> { - runChat.value = true + m.chatRunning.value = true } } } ) } else { - stopChat(m, runChat) + stopChat(m) } } -private fun stopChat(m: ChatModel, runChat: MutableState<Boolean?>) { +private fun stopChat(m: ChatModel) { withApi { try { - runChat.value = false stopChatAsync(m) - SimplexService.safeStopService(SimplexApp.context) - MessagesFetcherWorker.cancelAll() + platform.androidChatStopped() } catch (e: Error) { - runChat.value = true + m.chatRunning.value = true AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_stopping_chat), e.toString()) } } @@ -438,14 +435,14 @@ private fun exportArchive( chatArchiveName: MutableState<String?>, chatArchiveTime: MutableState<Instant?>, chatArchiveFile: MutableState<String?>, - saveArchiveLauncher: ManagedActivityResultLauncher<String, Uri?> + saveArchiveLauncher: FileChooserLauncher ) { progressIndicator.value = true withApi { try { val archiveFile = exportChatArchive(m, chatArchiveName, chatArchiveTime, chatArchiveFile) chatArchiveFile.value = archiveFile - saveArchiveLauncher.launch(archiveFile.substringAfterLast("/")) + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) progressIndicator.value = false } catch (e: Error) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString()) @@ -463,8 +460,8 @@ private suspend fun exportChatArchive( val archiveTime = Clock.System.now() val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant())) val archiveName = "simplex-chat.$ts.zip" - val archivePath = "${getFilesDirectory()}/$archiveName" - val config = ArchiveConfig(archivePath, parentTempDirectory = SimplexApp.context.cacheDir.toString()) + val archivePath = "${filesDir.absolutePath}${File.separator}$archiveName" + val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) m.controller.apiExportArchive(config) deleteOldArchive(m) m.controller.appPrefs.chatArchiveName.set(archiveName) @@ -478,7 +475,7 @@ private suspend fun exportChatArchive( private fun deleteOldArchive(m: ChatModel) { val chatArchiveName = m.controller.appPrefs.chatArchiveName.get() if (chatArchiveName != null) { - val file = File("${getFilesDirectory()}/$chatArchiveName") + val file = File("${filesDir.absolutePath}${File.separator}$chatArchiveName") val fileDeleted = file.delete() if (fileDeleted) { m.controller.appPrefs.chatArchiveName.set(null) @@ -489,39 +486,9 @@ private fun deleteOldArchive(m: ChatModel) { } } -@Composable -private fun rememberSaveArchiveLauncher(chatArchiveFile: MutableState<String?>): ManagedActivityResultLauncher<String, Uri?> = - rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument(), - onResult = { destination -> - val cxt = SimplexApp.context - try { - destination?.let { - val filePath = chatArchiveFile.value - if (filePath != null) { - val contentResolver = SimplexApp.context.contentResolver - contentResolver.openOutputStream(destination)?.let { stream -> - val outputStream = BufferedOutputStream(stream) - File(filePath).inputStream().use { it.copyTo(outputStream) } - outputStream.close() - Toast.makeText(cxt, generalGetString(MR.strings.file_saved), Toast.LENGTH_SHORT).show() - } - } else { - Toast.makeText(cxt, generalGetString(MR.strings.file_not_found), Toast.LENGTH_SHORT).show() - } - } - } catch (e: Error) { - Toast.makeText(cxt, generalGetString(MR.strings.error_saving_file), Toast.LENGTH_SHORT).show() - Log.e(TAG, "rememberSaveArchiveLauncher error saving archive $e") - } finally { - chatArchiveFile.value = null - } - } - ) - private fun importArchiveAlert( m: ChatModel, - importedArchiveUri: Uri, + importedArchiveURI: URI, appFilesCountAndSize: MutableState<Pair<Int, Long>>, progressIndicator: MutableState<Boolean> ) { @@ -529,28 +496,28 @@ private fun importArchiveAlert( title = generalGetString(MR.strings.import_database_question), text = generalGetString(MR.strings.your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one), confirmText = generalGetString(MR.strings.import_database_confirmation), - onConfirm = { importArchive(m, importedArchiveUri, appFilesCountAndSize, progressIndicator) }, + onConfirm = { importArchive(m, importedArchiveURI, appFilesCountAndSize, progressIndicator) }, destructive = true, ) } private fun importArchive( m: ChatModel, - importedArchiveUri: Uri, + importedArchiveURI: URI, appFilesCountAndSize: MutableState<Pair<Int, Long>>, progressIndicator: MutableState<Boolean> ) { progressIndicator.value = true - val archivePath = saveArchiveFromUri(importedArchiveUri) + val archivePath = saveArchiveFromURI(importedArchiveURI) if (archivePath != null) { withApi { try { m.controller.apiDeleteStorage() try { - val config = ArchiveConfig(archivePath, parentTempDirectory = SimplexApp.context.cacheDir.toString()) + val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) val archiveErrors = m.controller.apiImportArchive(config) DatabaseUtils.ksDatabasePassword.remove() - appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory()) + appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) if (archiveErrors.isEmpty()) { operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database)) @@ -576,21 +543,21 @@ private fun importArchive( } } -private fun saveArchiveFromUri(importedArchiveUri: Uri): String? { +private fun saveArchiveFromURI(importedArchiveURI: URI): String? { return try { - val inputStream = SimplexApp.context.contentResolver.openInputStream(importedArchiveUri) - val archiveName = getFileName(importedArchiveUri) + val inputStream = importedArchiveURI.inputStream() + val archiveName = getFileName(importedArchiveURI) if (inputStream != null && archiveName != null) { - val archivePath = "${SimplexApp.context.cacheDir}/$archiveName" + val archivePath = "$databaseExportDir${File.separator}$archiveName" val destFile = File(archivePath) - IOUtils.copy(inputStream, FileOutputStream(destFile)) + Files.copy(inputStream, destFile.toPath()) archivePath } else { - Log.e(TAG, "saveArchiveFromUri null inputStream") + Log.e(TAG, "saveArchiveFromURI null inputStream") null } } catch (e: Exception) { - Log.e(TAG, "saveArchiveFromUri error: ${e.message}") + Log.e(TAG, "saveArchiveFromURI error: ${e.message}") null } } @@ -647,10 +614,10 @@ private fun setCiTTL( private fun afterSetCiTTL( m: ChatModel, progressIndicator: MutableState<Boolean>, - appFilesCountAndSize: MutableState<Pair<Int, Long>> + appFilesCountAndSize: MutableState<Pair<Int, Long>>, ) { progressIndicator.value = false - appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory()) + appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) withApi { try { val chats = m.controller.apiGetChats() @@ -673,7 +640,7 @@ private fun deleteFilesAndMediaAlert(appFilesCountAndSize: MutableState<Pair<Int private fun deleteFiles(appFilesCountAndSize: MutableState<Pair<Int, Long>>) { deleteAppFiles() - appFilesCountAndSize.value = directoryFileCountAndSize(getAppFilesDirectory()) + appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) } private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean>, alert: () -> Unit) { @@ -682,12 +649,11 @@ private fun operationEnded(m: ChatModel, progressIndicator: MutableState<Boolean alert.invoke() } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewDatabaseLayout() { SimpleXTheme { @@ -697,8 +663,10 @@ fun PreviewDatabaseLayout() { chatDbChanged = false, useKeyChain = false, chatDbEncrypted = false, + passphraseSaved = false, initialRandomDBPassphrase = SharedPreference({ true }, {}), - importArchiveLauncher = rememberGetContentLauncher {}, + developerTools = true, + importArchiveLauncher = rememberFileChooserLauncher(true) {}, chatArchiveName = remember { mutableStateOf("dummy_archive") }, chatArchiveTime = remember { mutableStateOf(Clock.System.now()) }, chatLastStart = remember { mutableStateOf(Clock.System.now()) }, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt similarity index 76% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 9295fcd46e..d8466e9d96 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -9,14 +8,14 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* -import androidx.compose.ui.window.Dialog -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.ui.theme.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource @@ -54,26 +53,31 @@ class AlertManager { buttons: @Composable () -> Unit, ) { showAlert { - Dialog(onDismissRequest = this::hideAlert) { - Column( - Modifier - .background(MaterialTheme.colors.surface, RoundedCornerShape(corner = CornerSize(25.dp))) - .padding(bottom = DEFAULT_PADDING) - ) { + AlertDialog( + onDismissRequest = ::hideAlert, + title = { Text( title, Modifier.fillMaxWidth().padding(vertical = DEFAULT_PADDING), textAlign = TextAlign.Center, fontSize = 20.sp ) - CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { - if (text != null) { - Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary) + }, + buttons = { + Column( + Modifier + .padding(bottom = DEFAULT_PADDING) + ) { + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { + if (text != null) { + Text(text, Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), fontSize = 16.sp, textAlign = TextAlign.Center, color = MaterialTheme.colors.secondary) + } + buttons() } - buttons() } - } - } + }, + shape = RoundedCornerShape(corner = CornerSize(25.dp)) + ) } } @@ -97,6 +101,10 @@ class AlertManager { Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.SpaceBetween ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } TextButton(onClick = { onDismiss?.invoke() hideAlert() @@ -104,7 +112,7 @@ class AlertManager { TextButton(onClick = { onConfirm?.invoke() hideAlert() - }) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } + }, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -157,13 +165,22 @@ class AlertManager { title = alertTitle(title), text = alertText(text), buttons = { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } Row( Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.Center ) { - TextButton(onClick = { - hideAlert() - }) { Text(confirmText, color = Color.Unspecified) } + TextButton( + onClick = { + hideAlert() + }, + Modifier.focusRequester(focusRequester) + ) { + Text(confirmText, color = Color.Unspecified) + } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -203,7 +220,7 @@ private fun alertText(text: String?): (@Composable () -> Unit)? { } else { ({ Text( - text, + escapedHtmlToAnnotatedString(text, LocalDensity.current), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontSize = 16.sp, @@ -211,4 +228,4 @@ private fun alertText(text: String?): (@Composable () -> Unit)? { ) }) } -} \ No newline at end of file +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/AnimationUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt similarity index 87% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/AnimationUtils.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt index 15f507af62..6a400295ed 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/AnimationUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AnimationUtils.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.animation.core.* diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt similarity index 88% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index 175d27c7c4..3a799eddf8 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -1,5 +1,6 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -12,12 +13,11 @@ import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.ContentScale import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.ChatInfo -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.common.model.ChatInfo +import chat.simplex.common.platform.base64ToBitmap +import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -25,7 +25,7 @@ import dev.icerock.moko.resources.ImageResource fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { val icon = if (chatInfo is ChatInfo.Group) MR.images.ic_supervised_user_circle_filled - else MR.images.ic_account_circle_filled + else MR.images.ic_account_circle_filled ProfileImage(size, chatInfo.image, icon, iconColor) } @@ -70,7 +70,7 @@ fun ProfileImage( ) } } else { - val imageBitmap = base64ToBitmap(image).asImageBitmap() + val imageBitmap = base64ToBitmap(image) Image( imageBitmap, stringResource(MR.strings.image_descr_profile_image), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt new file mode 100644 index 0000000000..aa3c4560ea --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.kt @@ -0,0 +1,39 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.unit.dp + +sealed class AttachmentOption { + object CameraPhoto: AttachmentOption() + object GalleryImage: AttachmentOption() + object GalleryVideo: AttachmentOption() + object File: AttachmentOption() +} + +@Composable +fun ChooseAttachmentView(attachmentOption: MutableState<AttachmentOption?>, hide: () -> Unit) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onFocusChanged { focusState -> + if (!focusState.hasFocus) hide() + } + ) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 30.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ChooseAttachmentButtons(attachmentOption, hide) + } + } +} + +@Composable +expect fun ChooseAttachmentButtons(attachmentOption: MutableState<AttachmentOption?>, hide: () -> Unit) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt similarity index 84% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt index b7d42f493d..848b21eb44 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -11,13 +10,13 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import chat.simplex.app.ui.theme.* +import chat.simplex.common.ui.theme.* @Composable -fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> Unit = {}) { +fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}) { Column( Modifier .fillMaxWidth() @@ -33,7 +32,11 @@ fun CloseSheetBar(close: (() -> Unit)?, endButtons: @Composable RowScope.() -> U horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - NavigationButtonBack(onButtonClicked = close) + if (showClose) { + NavigationButtonBack(onButtonClicked = close) + } else { + Spacer(Modifier) + } Row { endButtons() } @@ -63,12 +66,11 @@ fun AppBarTitle(title: String, withPadding: Boolean = true, bottomPadding: Dp = ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewCloseSheetBar() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CustomIcons.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomIcons.kt similarity index 99% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CustomIcons.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomIcons.kt index 1ed7d3a954..e3ce25bd3a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CustomIcons.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomIcons.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.material.icons.materialIcon import androidx.compose.material.icons.materialPath diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt index 32c52dc1d5..fb1df940d9 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/CustomTimePicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CustomTimePicker.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -12,9 +12,9 @@ import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.model.CustomTimeUnit +import chat.simplex.common.model.timeText import chat.simplex.res.MR import com.sd.lib.compose.wheel_picker.* @@ -150,7 +150,7 @@ fun CustomTimePickerDialog( confirmButtonAction: (Int) -> Unit, cancel: () -> Unit ) { - Dialog(onDismissRequest = cancel) { + DefaultDialog(onDismissRequest = cancel) { Surface( shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DataClasses.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DataClasses.kt similarity index 57% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DataClasses.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DataClasses.kt index 9fdf2b3966..5eba757a25 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DataClasses.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DataClasses.kt @@ -1,4 +1,6 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers + +import androidx.compose.ui.text.AnnotatedString interface ValueTitle <T> { val value: T @@ -8,5 +10,5 @@ interface ValueTitle <T> { data class ValueTitleDesc <T> ( override val value: T, override val title: String, - val description: String + val description: AnnotatedString ): ValueTitle<T> diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt similarity index 78% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index f200d4ab69..e7da47f8f0 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -1,19 +1,13 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.util.Log -import chat.simplex.app.* -import chat.simplex.app.model.* -import chat.simplex.app.views.usersettings.Cryptor +import chat.simplex.common.model.* +import chat.simplex.common.platform.* import kotlinx.serialization.* import java.io.File import java.security.SecureRandom object DatabaseUtils { - private val cryptor = Cryptor() - - private val appPreferences: AppPreferences by lazy { - ChatController.appPrefs - } + private val appPreferences: AppPreferences = ChatController.appPrefs private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword" private const val APP_PASSWORD_ALIAS: String = "appPassword" @@ -26,16 +20,16 @@ object DatabaseUtils { class KeyStoreItem(private val alias: String, val passphrase: SharedPreference<String?>, val initVector: SharedPreference<String?>) { fun get(): String? { return cryptor.decryptData( - passphrase.get()?.toByteArrayFromBase64() ?: return null, - initVector.get()?.toByteArrayFromBase64() ?: return null, + passphrase.get()?.toByteArrayFromBase64ForPassphrase() ?: return null, + initVector.get()?.toByteArrayFromBase64ForPassphrase() ?: return null, alias, ) } fun set(key: String) { val data = cryptor.encryptText(key, alias) - passphrase.set(data.first.toBase64String()) - initVector.set(data.second.toBase64String()) + passphrase.set(data.first.toBase64StringForPassphrase()) + initVector.set(data.second.toBase64StringForPassphrase()) } fun remove() { @@ -46,20 +40,26 @@ object DatabaseUtils { } private fun hasDatabase(rootDir: String): Boolean = - File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists() + File(rootDir + File.separator + chatDatabaseFileName).exists() && File(rootDir + File.separator + agentDatabaseFileName).exists() fun useDatabaseKey(): String { Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}") var dbKey = "" val useKeychain = appPreferences.storeDBPassphrase.get() if (useKeychain) { - if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) { + if (!hasDatabase(dataDir.absolutePath)) { dbKey = randomDatabasePassword() ksDatabasePassword.set(dbKey) appPreferences.initialRandomDBPassphrase.set(true) } else { dbKey = ksDatabasePassword.get() ?: "" } + } else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) { + // In case of database was deleted by hand + dbKey = randomDatabasePassword() + ksDatabasePassword.set(dbKey) + appPreferences.initialRandomDBPassphrase.set(true) + appPreferences.storeDBPassphrase.set(true) } return dbKey } @@ -67,7 +67,7 @@ object DatabaseUtils { private fun randomDatabasePassword(): String { val s = ByteArray(32) SecureRandom().nextBytes(s) - return s.toBase64String().replace("\n", "") + return s.toBase64StringForPassphrase().replace("\n", "") } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt similarity index 95% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt index 538ce4d86a..71801e7a5f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultBasicTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultBasicTextField.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource @@ -21,8 +21,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.views.database.PassphraseStrength -import chat.simplex.app.views.database.validKey +import chat.simplex.common.views.database.PassphraseStrength +import chat.simplex.common.views.database.validKey import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -32,7 +32,7 @@ import kotlinx.coroutines.launch @Composable fun DefaultBasicTextField( modifier: Modifier, - initialValue: String, + state: MutableState<TextFieldValue>, placeholder: (@Composable () -> Unit)? = null, leadingIcon: (@Composable () -> Unit)? = null, focus: Boolean = false, @@ -41,11 +41,8 @@ fun DefaultBasicTextField( selectTextOnFocus: Boolean = false, keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions: KeyboardActions = KeyboardActions(), - onValueChange: (String) -> Unit, + onValueChange: (TextFieldValue) -> Unit, ) { - val state = remember { - mutableStateOf(TextFieldValue(initialValue)) - } val focusRequester = remember { FocusRequester() } val keyboard = LocalSoftwareKeyboardController.current @@ -83,8 +80,7 @@ fun DefaultBasicTextField( minHeight = TextFieldDefaults.MinHeight ), onValueChange = { - state.value = it - onValueChange(it.text) + onValueChange(it) }, cursorBrush = SolidColor(colors.cursorColor(false).value), visualTransformation = VisualTransformation.None, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.kt new file mode 100644 index 0000000000..7e916e0ea7 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.runtime.Composable + +@Composable +expect fun DefaultDialog( + onDismissRequest: () -> Unit, + content: @Composable () -> Unit +) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt similarity index 68% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt index 71af738bda..f3aec77e0b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultDropdownMenu.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultDropdownMenu.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -8,10 +8,28 @@ import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import chat.simplex.app.ui.theme.* + +expect fun Modifier.onRightClick(action: () -> Unit): Modifier + +expect interface DefaultExposedDropdownMenuBoxScope { + @Composable + open fun DefaultExposedDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit + ) +} + +@Composable +expect fun DefaultExposedDropdownMenuBox( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + content: @Composable DefaultExposedDropdownMenuBoxScope.() -> Unit +) @Composable fun DefaultDropdownMenu( @@ -25,7 +43,7 @@ fun DefaultDropdownMenu( DropdownMenu( expanded = showMenu.value, onDismissRequest = { showMenu.value = false }, - Modifier + modifier = Modifier .widthIn(min = 250.dp) .background(MaterialTheme.colors.surface) .padding(vertical = 4.dp), @@ -37,7 +55,7 @@ fun DefaultDropdownMenu( } @Composable -fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu( +fun DefaultExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu( expanded: MutableState<Boolean>, modifier: Modifier = Modifier, dropdownMenuItems: (@Composable () -> Unit)? @@ -45,7 +63,7 @@ fun ExposedDropdownMenuBoxScope.DefaultExposedDropdownMenu( MaterialTheme( shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(corner = CornerSize(25.dp))) ) { - ExposedDropdownMenu( + DefaultExposedDropdownMenu( modifier = Modifier .widthIn(min = 200.dp) .background(MaterialTheme.colors.surface) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt index 613caed54d..75abc67b46 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultSwitch.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material.* diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 44a56c441d..0162ac7e78 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -1,6 +1,5 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import chat.simplex.app.R import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -11,7 +10,7 @@ import androidx.compose.ui.graphics.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.ui.theme.* +import chat.simplex.common.ui.theme.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Enums.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt similarity index 61% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Enums.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt index cad5d4c53b..67f82e5279 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Enums.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Enums.kt @@ -1,18 +1,18 @@ @file:UseSerializers(UriSerializer::class) -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.net.Uri import androidx.compose.runtime.saveable.Saver import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import java.net.URI sealed class SharedContent { data class Text(val text: String): SharedContent() - data class Media(val text: String, val uris: List<Uri>): SharedContent() - data class File(val text: String, val uri: Uri): SharedContent() + data class Media(val text: String, val uris: List<URI>): SharedContent() + data class File(val text: String, val uri: URI): SharedContent() } enum class AnimatedViewState { @@ -37,16 +37,16 @@ enum class AnimatedViewState { } -@Serializer(forClass = Uri::class) -object UriSerializer : KSerializer<Uri> { - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Uri", PrimitiveKind.STRING) - override fun serialize(encoder: Encoder, value: Uri) = encoder.encodeString(value.toString()) - override fun deserialize(decoder: Decoder): Uri = Uri.parse(decoder.decodeString()) +@Serializer(forClass = URI::class) +object UriSerializer : KSerializer<URI> { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("URI", PrimitiveKind.STRING) + override fun serialize(encoder: Encoder, value: URI) = encoder.encodeString(value.toString()) + override fun deserialize(decoder: Decoder): URI = URI(decoder.decodeString()) } @Serializable sealed class UploadContent { - @Serializable data class SimpleImage(val uri: Uri): UploadContent() - @Serializable data class AnimatedImage(val uri: Uri): UploadContent() - @Serializable data class Video(val uri: Uri, val duration: Int): UploadContent() + @Serializable data class SimpleImage(val uri: URI): UploadContent() + @Serializable data class AnimatedImage(val uri: URI): UploadContent() + @Serializable data class Video(val uri: URI, val duration: Int): UploadContent() } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index eb563dbd0f..2f24ff4144 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -12,10 +12,9 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.usersettings.SettingsActionItemWithContent @Composable fun <T> ExposedDropDownSettingRow( @@ -30,7 +29,7 @@ fun <T> ExposedDropDownSettingRow( ) { SettingsActionItemWithContent(icon, title, iconColor = iconTint, disabled = !enabled.value) { val expanded = remember { mutableStateOf(false) } - ExposedDropdownMenuBox( + DefaultExposedDropdownMenuBox( expanded = expanded.value, onExpandedChange = { expanded.value = !expanded.value && enabled.value diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt similarity index 95% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt index daf323dc0d..9252e8b032 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/GestureDetector.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GestureDetector.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.util.Log +import chat.simplex.common.platform.Log import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.* @@ -28,7 +28,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.* import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.unit.Density -import chat.simplex.app.TAG +import chat.simplex.common.platform.TAG import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlin.math.PI @@ -92,6 +92,17 @@ suspend fun PointerInputScope.detectGesture( } } +suspend fun PointerInputScope.detectCursorMove(onMove: (Offset) -> Unit = {},) = coroutineScope { + forEachGesture { + awaitPointerEventScope { + val event = awaitPointerEvent() + if (event.type == PointerEventType.Move) { + onMove(event.changes[0].position) + } + } + } +} + private suspend fun AwaitPointerEventScope.consumeUntilUp() { do { val event = awaitPointerEvent() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GetImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GetImageView.kt new file mode 100644 index 0000000000..308038ec5e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/GetImageView.kt @@ -0,0 +1,13 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.ImageBitmap +import java.net.URI + +@Composable +expect fun GetImageBottomSheet( + imageBitmap: MutableState<URI?>, + onImageChange: (ImageBitmap) -> Unit, + hideBottomSheet: () -> Unit +) + diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index 47efef1da3..93d0a56766 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/LinkPreviews.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.content.res.Configuration -import android.graphics.BitmapFactory +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -10,17 +9,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.layout.ContentScale import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.LinkPreview -import chat.simplex.app.ui.theme.* +import chat.simplex.common.model.LinkPreview +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -58,11 +55,11 @@ suspend fun getLinkPreview(url: String): LinkPreview? { imageUri = normalizeImageUri(u, imageUri) try { val stream = URL(imageUri).openStream() - val image = resizeImageToStrSize(BitmapFactory.decodeStream(stream), maxDataSize = 14000) -// TODO add once supported in iOS -// val description = ogTags.firstOrNull { -// it.attr("property") == "og:description" -// }?.attr("content") ?: "" + val image = resizeImageToStrSize(stream.use(::loadImageBitmap), maxDataSize = 14000) + // TODO add once supported in iOS + // val description = ogTags.firstOrNull { + // it.attr("property") == "og:description" + // }?.attr("content") ?: "" if (title != null) { return@withContext LinkPreview(url, title, description = "", image) } @@ -96,7 +93,7 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel ) } } else { - val imageBitmap = base64ToBitmap(linkPreview.image).asImageBitmap() + val imageBitmap = base64ToBitmap(linkPreview.image) Image( imageBitmap, stringResource(MR.strings.image_descr_link_preview), @@ -125,9 +122,9 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel @Composable fun ChatItemLinkView(linkPreview: LinkPreview) { - Column { + Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { Image( - base64ToBitmap(linkPreview.image).asImageBitmap(), + base64ToBitmap(linkPreview.image), stringResource(MR.strings.image_descr_link_preview), modifier = Modifier.fillMaxWidth(), contentScale = ContentScale.FillWidth, @@ -174,12 +171,11 @@ private fun normalizeImageUri(u: URL, imageUri: String) = when { } }*/ -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "ChatItemLinkView (Dark Mode)" -) +)*/ @Composable fun PreviewChatItemLinkView() { SimpleXTheme { @@ -187,12 +183,11 @@ fun PreviewChatItemLinkView() { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "ComposeLinkView (Dark Mode)" -) +)*/ @Composable fun PreviewComposeLinkView() { SimpleXTheme { @@ -200,7 +195,7 @@ fun PreviewComposeLinkView() { } } -@Preview(showBackground = true) +@Preview @Composable fun PreviewComposeLinkViewLoading() { SimpleXTheme { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt new file mode 100644 index 0000000000..8e6c9fffc1 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.kt @@ -0,0 +1,60 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.ui.Modifier +import chat.simplex.common.model.ChatController +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.BackHandler +import chat.simplex.common.views.localauth.LocalAuthView +import chat.simplex.common.views.usersettings.LAMode +import chat.simplex.res.MR + +sealed class LAResult { + object Success: LAResult() + class Error(val errString: CharSequence): LAResult() + class Failed(val errString: CharSequence? = null): LAResult() + class Unavailable(val errString: CharSequence? = null): LAResult() +} + +data class LocalAuthRequest ( + val title: String?, + val reason: String, + val password: String, + val selfDestruct: Boolean, + val completed: (LAResult) -> Unit +) { + companion object { + val sample = LocalAuthRequest(generalGetString(MR.strings.la_enter_app_passcode), generalGetString(MR.strings.la_authenticate), "", selfDestruct = false) { } + } +} + +expect fun authenticate( + promptTitle: String, + promptSubtitle: String, + selfDestruct: Boolean = false, + usingLAMode: LAMode = ChatModel.controller.appPrefs.laMode.get(), + completed: (LAResult) -> Unit +) + +fun authenticateWithPasscode( + promptTitle: String, + promptSubtitle: String, + selfDestruct: Boolean, + completed: (LAResult) -> Unit +) { + val password = DatabaseUtils.ksAppPassword.get() ?: return completed(LAResult.Unavailable(generalGetString(MR.strings.la_no_app_password))) + ModalManager.fullscreen.showPasscodeCustomModal { close -> + BackHandler { + close() + completed(LAResult.Error(generalGetString(MR.strings.authentication_cancelled))) + } + Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + LocalAuthView(ChatModel, LocalAuthRequest(promptTitle, promptSubtitle, password, selfDestruct && ChatController.appPrefs.selfDestruct.get()) { + close() + completed(it) + }) + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt similarity index 69% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ModalView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 724f0907fa..0f930b3126 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -1,59 +1,58 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.R -import android.util.Log -import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import dev.icerock.moko.resources.compose.painterResource -import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.text.capitalize -import androidx.compose.ui.text.intl.Locale -import chat.simplex.app.TAG -import chat.simplex.app.ui.theme.isInDarkTheme -import chat.simplex.app.ui.theme.themedBackground +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* import java.util.concurrent.atomic.AtomicBoolean @Composable fun ModalView( close: () -> Unit, + showClose: Boolean = true, background: Color = MaterialTheme.colors.background, modifier: Modifier = Modifier, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit, ) { - BackHandler(onBack = close) + if (showClose) { + BackHandler(onBack = close) + } Surface(Modifier.fillMaxSize()) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - CloseSheetBar(close, endButtons) + CloseSheetBar(close, showClose, endButtons) Box(modifier) { content() } } } } -class ModalManager { +enum class ModalPlacement { + START, CENTER, END, FULLSCREEN +} + +class ModalManager(private val placement: ModalPlacement? = null) { private val modalViews = arrayListOf<Pair<Boolean, (@Composable (close: () -> Unit) -> Unit)>>() private val modalCount = mutableStateOf(0) private val toRemove = mutableSetOf<Int>() private var oldViewChanging = AtomicBoolean(false) private var passcodeView: MutableState<(@Composable (close: () -> Unit) -> Unit)?> = mutableStateOf(null) - fun showModal(settings: Boolean = false, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) { + fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit) { showCustomModal { close -> - ModalView(close, endButtons = endButtons, content = content) + ModalView(close, showClose = showClose, endButtons = endButtons, content = content) } } - fun showModalCloseable(settings: Boolean = false, content: @Composable (close: () -> Unit) -> Unit) { + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, content: @Composable (close: () -> Unit) -> Unit) { showCustomModal { close -> - ModalView(close, content = { content(close) }) + ModalView(close, showClose = showClose, content = { content(close) }) } } @@ -64,8 +63,17 @@ class ModalManager { if (toRemove.isNotEmpty()) { runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } } } - modalViews.add(animated to modal) + // Make animated appearance only on Android (everytime) and on Desktop (when it's on the start part of the screen or modals > 0) + // to prevent unneeded animation on different situations + val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START) + modalViews.add(anim to modal) modalCount.value = modalViews.size - toRemove.size + + if (placement == ModalPlacement.CENTER) { + ChatModel.chatId.value = null + } else if (placement == ModalPlacement.END) { + desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH) + } } fun showPasscodeCustomModal(modal: @Composable (close: () -> Unit) -> Unit) { @@ -89,6 +97,12 @@ class ModalManager { modalCount.value = 0 } + fun closeModalsExceptFirst() { + while (modalCount.value > 1) { + closeModal() + } + } + @OptIn(ExperimentalAnimationApi::class) @Composable fun showInView() { @@ -155,6 +169,17 @@ private fun <T> animationSpec() = tween<T>(durationMillis = 250, easing = FastOu // private fun <T> animationSpecFromEnd() = tween<T>(durationMillis = 100, easing = FastOutSlowInEasing) companion object { - val shared = ModalManager() + private val shared = ModalManager() + val start = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.START) + val center = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.CENTER) + val end = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.END) + val fullscreen = if (appPlatform.isAndroid) shared else ModalManager(ModalPlacement.FULLSCREEN) + + fun closeAllModalsEverywhere() { + start.closeModals() + center.closeModals() + end.closeModals() + fullscreen.closeModals() + } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt index f60d644a88..6990a69ebd 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Modifiers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.offset diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt similarity index 98% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index a7afb9441f..4b6c70df44 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource @@ -24,7 +24,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R import chat.simplex.res.MR import kotlinx.coroutines.delay diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Section.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index 5c7a40e282..6adbfed76c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -1,5 +1,4 @@ -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* @@ -8,16 +7,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ValueTitleDesc -import chat.simplex.app.views.helpers.ValueTitle -import chat.simplex.app.views.usersettings.SettingsActionItemWithContent +import chat.simplex.common.platform.windowWidth +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR @Composable @@ -132,7 +130,9 @@ fun SectionItemViewSpaceBetween( .fillMaxWidth() .sizeIn(minHeight = minHeight) Row( - if (click == null || disabled) modifier.padding(padding) else modifier.combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding), + if (click == null || disabled) modifier.padding(padding) else modifier + .combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding) + .onRightClick { onLongClick?.invoke() }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { @@ -238,13 +238,13 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color @Composable fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) { SectionItemViewSpaceBetween(onClick) { - val configuration = LocalConfiguration.current + val screenWidthDp = windowWidth() Text(title) Text( value, Modifier .padding(start = 10.dp) - .widthIn(max = (configuration.screenWidthDp / 2).dp), + .widthIn(max = (screenWidthDp / 2)), maxLines = 1, overflow = TextOverflow.Ellipsis, color = MaterialTheme.colors.secondary diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt similarity index 87% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt index 6202ded6de..7db001a4bd 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt @@ -1,5 +1,6 @@ -package chat.simplex.app.ui.theme +package chat.simplex.common.views.helpers +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -11,10 +12,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.res.MR @Composable @@ -65,11 +67,13 @@ fun SimpleButton( fun SimpleButtonIconEnded( text: String, icon: Painter, + style: TextStyle = MaterialTheme.typography.caption, color: Color = MaterialTheme.colors.primary, + disabled: Boolean = false, click: () -> Unit ) { - SimpleButtonFrame(click) { - Text(text, style = MaterialTheme.typography.caption, color = color) + SimpleButtonFrame(click, disabled = disabled) { + Text(text, style = style, color = color) Icon( icon, text, tint = color, modifier = Modifier.padding(start = 8.dp) @@ -90,7 +94,7 @@ fun SimpleButtonFrame(click: () -> Unit, modifier: Modifier = Modifier, disabled @Preview @Composable -fun PreviewCloseSheetBar() { +fun PreviewShareButton() { SimpleXTheme { SimpleButton(text = "Share", icon = painterResource(MR.images.ic_share), click = {}) } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt similarity index 83% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt index 9076c8ecdc..45accccc59 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/helpers/TextEditor.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/TextEditor.kt @@ -1,11 +1,12 @@ -package chat.simplex.app.views.helpers +package chat.simplex.common.views.helpers -import android.util.Log +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -13,16 +14,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.* -import chat.simplex.app.TAG -import chat.simplex.app.chatParseMarkdown -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.MarkdownText -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.model.FormattedText +import chat.simplex.common.platform.* import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.serialization.Serializable import java.lang.Exception @@ -53,19 +51,20 @@ fun TextEditor( Modifier .fillMaxWidth() .padding(contentPadding) - .heightIn(min = 52.dp), -// .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(26.dp)), + .heightIn(min = 52.dp) + .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(14.dp)), contentAlignment = Alignment.Center, ) { - val modifier = modifier + val textFieldModifier = modifier .fillMaxWidth() .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } + .padding(10.dp) BasicTextField( value = value.value, onValueChange = { value.value = it }, - modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), + modifier = if (focusRequester == null) textFieldModifier else textFieldModifier.focusRequester(focusRequester), textStyle = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.onBackground, lineHeight = 22.sp), keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt new file mode 100644 index 0000000000..2aad0bc3d0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -0,0 +1,409 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.Saver +import androidx.compose.ui.graphics.* +import androidx.compose.ui.platform.* +import androidx.compose.ui.text.* +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeOverrides +import chat.simplex.common.views.chatlist.connectIfOpenedViaUri +import chat.simplex.res.MR +import com.charleskorn.kaml.decodeFromStream +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import java.io.* +import java.net.URI +import java.nio.file.Files +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.* + +fun withApi(action: suspend CoroutineScope.() -> Unit): Job = withScope(GlobalScope, action) + +fun withScope(scope: CoroutineScope, action: suspend CoroutineScope.() -> Unit): Job = + scope.launch { withContext(Dispatchers.Main, action) } + +fun withBGApi(action: suspend CoroutineScope.() -> Unit): Job = + CoroutineScope(Dispatchers.Default).launch(block = action) + +enum class KeyboardState { + Opened, Closed +} + +// Resource to annotated string from +// https://stackoverflow.com/questions/68549248/android-jetpack-compose-how-to-show-styled-text-from-string-resources +fun generalGetString(id: StringResource): String { + // prefer stringResource in Composable items to retain preview abilities + return id.localized() +} + +expect fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString + +@Composable +fun annotatedStringResource(id: StringResource): AnnotatedString { + val density = LocalDensity.current + return remember(id) { + escapedHtmlToAnnotatedString(id.localized(), density) + } +} + +// maximum image file size to be auto-accepted +const val MAX_IMAGE_SIZE: Long = 261_120 // 255KB +const val MAX_IMAGE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 +const val MAX_VOICE_SIZE_AUTO_RCV: Long = MAX_IMAGE_SIZE * 2 +const val MAX_VIDEO_SIZE_AUTO_RCV: Long = 1_047_552 // 1023KB + +const val MAX_VOICE_MILLIS_FOR_SENDING: Int = 300_000 + +const val MAX_FILE_SIZE_SMP: Long = 8000000 + +const val MAX_FILE_SIZE_XFTP: Long = 1_073_741_824 // 1GB + +expect fun getAppFileUri(fileName: String): URI + +// https://developer.android.com/training/data-storage/shared/documents-files#bitmap +expect fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? + +expect fun getFileName(uri: URI): String? + +expect fun getAppFilePath(uri: URI): String? + +expect fun getFileSize(uri: URI): Long? + +expect fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean = true): ImageBitmap? + +expect fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? + +expect fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean = true): Any? + +fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverrides? { + uri.inputStream().use { + runCatching { + return yaml.decodeFromStream<ThemeOverrides>(it!!) + }.onFailure { + if (withAlertOnException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.import_theme_error), + text = generalGetString(MR.strings.import_theme_error_desc), + ) + } + } + } + return null +} + +fun saveImage(uri: URI, encrypted: Boolean): CryptoFile? { + val bitmap = getBitmapFromUri(uri) ?: return null + return saveImage(bitmap, encrypted) +} + +fun saveImage(image: ImageBitmap, encrypted: Boolean): CryptoFile? { + return try { + val ext = if (image.hasAlpha()) "png" else "jpg" + val dataResized = resizeImageToDataSize(image, ext == "png", maxDataSize = MAX_IMAGE_SIZE) + val destFileName = generateNewFileName("IMG", ext) + val destFile = File(getAppFilePath(destFileName)) + if (encrypted) { + val args = writeCryptoFile(destFile.absolutePath, dataResized.toByteArray()) + CryptoFile(destFileName, args) + } else { + val output = FileOutputStream(destFile) + dataResized.writeTo(output) + output.flush() + output.close() + CryptoFile.plain(destFileName) + } + } catch (e: Exception) { + Log.e(TAG, "Util.kt saveImage error: ${e.stackTraceToString()}") + null + } +} + +fun saveAnimImage(uri: URI, encrypted: Boolean): CryptoFile? { + return try { + val filename = getFileName(uri)?.lowercase() + var ext = when { + // remove everything but extension + filename?.contains(".") == true -> filename.replaceBeforeLast('.', "").replace(".", "") + else -> "gif" + } + // Just in case the image has a strange extension + if (ext.length < 3 || ext.length > 4) ext = "gif" + val destFileName = generateNewFileName("IMG", ext) + val destFile = File(getAppFilePath(destFileName)) + if (encrypted) { + val args = writeCryptoFile(destFile.absolutePath, uri.inputStream()?.readAllBytes() ?: return null) + CryptoFile(destFileName, args) + } else { + Files.copy(uri.inputStream(), destFile.toPath()) + CryptoFile.plain(destFileName) + } + } catch (e: Exception) { + Log.e(TAG, "Util.kt saveAnimImage error: ${e.message}") + null + } +} + +expect suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? + +fun saveFileFromUri(uri: URI, encrypted: Boolean): CryptoFile? { + return try { + val inputStream = uri.inputStream() + val fileToSave = getFileName(uri) + return if (inputStream != null && fileToSave != null) { + val destFileName = uniqueCombine(fileToSave) + val destFile = File(getAppFilePath(destFileName)) + if (encrypted) { + createTmpFileAndDelete { tmpFile -> + Files.copy(inputStream, tmpFile.toPath()) + val args = encryptCryptoFile(tmpFile.absolutePath, destFile.absolutePath) + CryptoFile(destFileName, args) + } + } else { + Files.copy(inputStream, destFile.toPath()) + CryptoFile.plain(destFileName) + } + } else { + Log.e(TAG, "Util.kt saveFileFromUri null inputStream") + null + } + } catch (e: Exception) { + Log.e(TAG, "Util.kt saveFileFromUri error: ${e.stackTraceToString()}") + null + } +} + +fun <T> createTmpFileAndDelete(onCreated: (File) -> T): T { + val tmpFile = File(tmpDir, UUID.randomUUID().toString()) + tmpFile.deleteOnExit() + ChatModel.filesToDelete.add(tmpFile) + try { + return onCreated(tmpFile) + } finally { + tmpFile.delete() + } +} + +fun generateNewFileName(prefix: String, ext: String): String { + val sdf = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US) + sdf.timeZone = TimeZone.getTimeZone("GMT") + val timestamp = sdf.format(Date()) + return uniqueCombine("${prefix}_$timestamp.$ext") +} + +fun uniqueCombine(fileName: String): String { + val orig = File(fileName) + val name = orig.nameWithoutExtension + val ext = orig.extension + fun tryCombine(n: Int): String { + val suffix = if (n == 0) "" else "_$n" + val f = "$name$suffix.$ext" + return if (File(getAppFilePath(f)).exists()) tryCombine(n + 1) else f + } + return tryCombine(0) +} + +fun formatBytes(bytes: Long): String { + if (bytes == 0.toLong()) { + return "0 bytes" + } + val bytesDouble = bytes.toDouble() + val k = 1024.toDouble() + val units = arrayOf("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + val i = floor(log2(bytesDouble) / log2(k)) + val size = bytesDouble / k.pow(i) + val unit = units[i.toInt()] + + return if (i <= 1) { + String.format("%.0f %s", size, unit) + } else { + String.format("%.2f %s", size, unit) + } +} + +fun removeFile(fileName: String): Boolean { + val file = File(getAppFilePath(fileName)) + val fileDeleted = file.delete() + if (!fileDeleted) { + Log.e(TAG, "Util.kt removeFile error") + } + return fileDeleted +} + +fun deleteAppFiles() { + val dir = appFilesDir + try { + dir.list()?.forEach { + removeFile(it) + } + } catch (e: java.lang.Exception) { + Log.e(TAG, "Util deleteAppFiles error: ${e.stackTraceToString()}") + } +} + +fun directoryFileCountAndSize(dir: String): Pair<Int, Long> { // count, size in bytes + var fileCount = 0 + var bytes = 0L + try { + File(dir).listFiles()?.forEach { + fileCount++ + bytes += it.length() + } + } catch (e: Exception) { + Log.e(TAG, "Util directoryFileCountAndSize error: ${e.stackTraceToString()}") + } + return fileCount to bytes +} + +fun getMaxFileSize(fileProtocol: FileProtocol): Long { + return when (fileProtocol) { + FileProtocol.XFTP -> MAX_FILE_SIZE_XFTP + FileProtocol.SMP -> MAX_FILE_SIZE_SMP + } +} + +expect suspend fun getBitmapFromVideo(uri: URI, timestamp: Long? = null, random: Boolean = true): VideoPlayerInterface.PreviewAndDuration + +fun Color.darker(factor: Float = 0.1f): Color = + Color(max(red * (1 - factor), 0f), max(green * (1 - factor), 0f), max(blue * (1 - factor), 0f), alpha) + +fun Color.lighter(factor: Float = 0.1f): Color = + Color(min(red * (1 + factor), 1f), min(green * (1 + factor), 1f), min(blue * (1 + factor), 1f), alpha) + +fun Color.mixWith(color: Color, alpha: Float): Color = blendARGB(color, this, alpha) + +fun blendARGB( + color1: Color, color2: Color, + ratio: Float +): Color { + val inverseRatio = 1 - ratio + val a: Float = color1.alpha * inverseRatio + color2.alpha * ratio + val r: Float = color1.red * inverseRatio + color2.red * ratio + val g: Float = color1.green * inverseRatio + color2.green * ratio + val b: Float = color1.blue * inverseRatio + color2.blue * ratio + return Color(r, g, b, a) +} + +fun InputStream.toByteArray(): ByteArray = + ByteArrayOutputStream().use { output -> + val b = ByteArray(4096) + var n = read(b) + while (n != -1) { + output.write(b, 0, n); + n = read(b) + } + return output.toByteArray() + } + +expect fun ByteArray.toBase64StringForPassphrase(): String + +// Android's default implementation that was used before multiplatform, adds non-needed characters at the end of string +// which can be bypassed by: +// fun String.toByteArrayFromBase64(): ByteArray = Base64.getDecoder().decode(this.trimEnd { it == '\n' || it == ' ' }) +expect fun String.toByteArrayFromBase64ForPassphrase(): ByteArray + +val LongRange.Companion.saver + get() = Saver<MutableState<LongRange>, Pair<Long, Long>>( + save = { it.value.first to it.value.last }, + restore = { mutableStateOf(it.first..it.second) } + ) + +/* Make sure that T class has @Serializable annotation */ +inline fun <reified T> serializableSaver(): Saver<T, *> = Saver( + save = { json.encodeToString(it) }, + restore = { json.decodeFromString(it) } +) + +fun UriHandler.openVerifiedSimplexUri(uri: String) { + val URI = try { URI.create(uri) } catch (e: Exception) { null } + if (URI != null) { + connectIfOpenedViaUri(URI, ChatModel) + } +} + +fun UriHandler.openUriCatching(uri: String) { + try { + openUri(uri) + } catch (e: Exception/*ActivityNotFoundException*/) { + Log.e(TAG, e.stackTraceToString()) + } +} + +fun IntSize.Companion.Saver(): Saver<IntSize, *> = Saver( + save = { it.width to it.height }, + restore = { IntSize(it.first, it.second) } +) + +@Composable +fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { + DisposableEffect(Unit) { + always() + val orientation = windowOrientation() + onDispose { + whenDispose() + if (orientation == windowOrientation()) { + whenGone() + } + } + } +} + +@Composable +fun DisposableEffectOnRotate(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenRotate: () -> Unit) { + DisposableEffect(Unit) { + always() + val orientation = windowOrientation() + onDispose { + whenDispose() + if (orientation != windowOrientation()) { + whenRotate() + } + } + } +} + +/** + * Runs the [block] only after initial value of the [key1] changes, not after initial launch + * */ +@Composable +@NonRestartableComposable +fun <T> KeyChangeEffect( + key1: T?, + block: suspend CoroutineScope.(prevKey: T?) -> Unit +) { + var prevKey by remember { mutableStateOf(key1) } + var anyChange by remember { mutableStateOf(false) } + LaunchedEffect(key1) { + if (anyChange || key1 != prevKey) { + block(prevKey) + prevKey = key1 + anyChange = true + } + } +} + +/** + * Runs the [block] only after initial value of the [key1] or [key2] changes, not after initial launch + * */ +@Composable +@NonRestartableComposable +fun KeyChangeEffect( + key1: Any?, + key2: Any?, + block: suspend CoroutineScope.() -> Unit +) { + val initialKey = remember { key1 } + val initialKey2 = remember { key2 } + var anyChange by remember { mutableStateOf(false) } + LaunchedEffect(key1) { + if (anyChange || key1 != initialKey || key2 != initialKey2) { + block() + anyChange = true + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt similarity index 77% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/LocalAuthView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index 9187c85d9b..8b5c2a8336 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -1,20 +1,19 @@ -package chat.simplex.app.views.localauth +package chat.simplex.common.views.localauth -import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.* -import chat.simplex.app.model.* -import chat.simplex.app.views.database.deleteChatAsync -import chat.simplex.app.views.database.stopChatAsync -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword -import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword -import chat.simplex.app.views.onboarding.OnboardingStage +import chat.simplex.common.views.database.deleteChatAsync +import chat.simplex.common.views.database.stopChatAsync +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword +import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.common.views.onboarding.OnboardingStage +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.Profile +import chat.simplex.common.platform.* import chat.simplex.res.MR -import kotlinx.coroutines.delay @Composable fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { @@ -43,7 +42,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( deleteChatAsync(m) ksAppPassword.set(password) ksSelfDestructPassword.remove() - m.controller.ntfManager.cancelAllNotifications() + ntfManager.cancelAllNotifications() val selfDestructPref = m.controller.appPrefs.selfDestruct val displayNamePref = m.controller.appPrefs.selfDestructDisplayName val displayName = displayNamePref.get() @@ -52,7 +51,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( m.chatDbChanged.value = true m.chatDbStatus.value = null try { - SimplexApp.context.initChatController(startChat = true) + initChatController(startChat = true) } catch (e: Exception) { Log.d(TAG, "initializeChat ${e.stackTraceToString()}") } @@ -67,11 +66,10 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: ( val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true) m.currentUser.value = createdUser m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete) - m.onboardingStage.value = OnboardingStage.OnboardingComplete if (createdUser != null) { m.controller.startChat(createdUser) } - ModalManager.shared.closeModals() + ModalManager.fullscreen.closeModals() AlertManager.shared.hideAlert() completed(LAResult.Success) } catch (e: Exception) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/PasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt similarity index 59% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/PasscodeView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt index 42186f20c9..4784951ad0 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/PasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasscodeView.kt @@ -1,19 +1,19 @@ -package chat.simplex.app.views.localauth +package chat.simplex.common.views.localauth -import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.focus.* +import androidx.compose.ui.input.key.* import dev.icerock.moko.resources.compose.painterResource import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.ui.theme.SimpleButton -import chat.simplex.app.views.helpers.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.SimpleButton +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable @@ -26,9 +26,43 @@ fun PasscodeView( submit: () -> Unit, cancel: () -> Unit, ) { + val focusRequester = remember { FocusRequester() } + + @Composable + fun Modifier.handleKeyboard(): Modifier { + val numbers = remember { + arrayOf( + Key.Zero, Key.One, Key.Two, Key.Three, Key.Four, Key.Five, Key.Six, Key.Seven, Key.Eight, Key.Nine, + Key.NumPad0, Key.NumPad1, Key.NumPad2, Key.NumPad3, Key.NumPad4, Key.NumPad5, Key.NumPad6, Key.NumPad7, Key.NumPad8, Key.NumPad9 + ) + } + return onPreviewKeyEvent { + if (it.key in numbers && it.type == KeyEventType.KeyDown) { + if (passcode.value.length < 16) { + passcode.value += numbers.indexOf(it.key) % 10 + } + true + } else if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown && (it.isCtrlPressed || it.isMetaPressed)) { + passcode.value = "" + true + } else if (it.key == Key.Backspace && it.type == KeyEventType.KeyDown) { + passcode.value = passcode.value.dropLast(1) + true + } else if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { + if ((submitEnabled?.invoke(passcode.value) != false && passcode.value.length >= 4)) { + submit() + } + true + } else { + false + } + } + } + @Composable fun VerticalLayout() { Column( + Modifier.handleKeyboard().focusRequester(focusRequester), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly ) { @@ -39,7 +73,7 @@ fun PasscodeView( } } PasscodeEntry(passcode, true) - Row { + Row(Modifier.heightIn(min = 70.dp), verticalAlignment = Alignment.CenterVertically) { SimpleButton(generalGetString(MR.strings.cancel_verb), icon = painterResource(MR.images.ic_close), click = cancel) Spacer(Modifier.size(20.dp)) SimpleButton(submitLabel, icon = painterResource(MR.images.ic_done_filled), disabled = submitEnabled?.invoke(passcode.value) == false || passcode.value.length < 4, click = submit) @@ -49,9 +83,9 @@ fun PasscodeView( @Composable fun HorizontalLayout() { - Row(Modifier.padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.Center) { + Row(Modifier.padding(horizontal = DEFAULT_PADDING).handleKeyboard().focusRequester(focusRequester), horizontalArrangement = Arrangement.Center) { Column( - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4), + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween ) { @@ -65,7 +99,7 @@ fun PasscodeView( } Column( - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING * 4), + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, top = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween ) { @@ -91,9 +125,14 @@ fun PasscodeView( } } - if (LocalContext.current.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + if (windowOrientation() == WindowOrientation.PORTRAIT || appPlatform.isDesktop) { VerticalLayout() } else { HorizontalLayout() } + LaunchedEffect(Unit) { + focusRequester.requestFocus() + // Disallow to steal a focus by clicking on buttons or using Tab + focusRequester.captureFocus() + } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt index 98a880b4f0..f76b82c31e 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/PasswordEntry.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/PasswordEntry.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views.localauth +package chat.simplex.common.views.localauth -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* @@ -16,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR @Composable @@ -39,7 +39,7 @@ fun PasscodeEntry( fun PasscodeView(password: MutableState<String>) { var showPasscode by rememberSaveable { mutableStateOf(false) } Text( - if (password.value.isEmpty()) " " else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) }, + if (password.value.isEmpty()) "" else remember(password.value, showPasscode) { splitPassword(showPasscode, password.value) }, Modifier.padding(vertical = 10.dp).clickable { showPasscode = !showPasscode }, style = MaterialTheme.typography.body1 ) @@ -47,7 +47,7 @@ fun PasscodeView(password: MutableState<String>) { @Composable private fun BoxWithConstraintsScope.VerticalPasswordGrid(password: MutableState<String>) { - val s = minOf(maxWidth, maxHeight) / 4 - 1.dp + val s = if (appPlatform.isAndroid) minOf(maxWidth, maxHeight) / 4 - 1.dp else minOf(minOf(maxWidth, maxHeight) / 4 - 1.dp, 100.dp) Column(Modifier.width(IntrinsicSize.Min)) { DigitsRow(s, 1, 2, 3, password) Divider() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/SetAppPasscodeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt similarity index 84% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/SetAppPasscodeView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt index 5bac848f76..18437dbf98 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/localauth/SetAppPasscodeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/SetAppPasscodeView.kt @@ -1,12 +1,11 @@ -package chat.simplex.app.views.localauth +package chat.simplex.common.views.localauth -import androidx.activity.compose.BackHandler import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import chat.simplex.app.R -import chat.simplex.app.views.helpers.DatabaseUtils -import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.platform.BackHandler +import chat.simplex.common.views.helpers.DatabaseUtils +import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddContactLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt similarity index 74% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddContactLearnMore.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt index dd1963921d..2913f6ac79 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddContactLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactLearnMore.kt @@ -1,14 +1,13 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat import androidx.compose.foundation.* import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.views.helpers.AppBarTitle -import chat.simplex.app.views.onboarding.ReadableText -import chat.simplex.app.views.onboarding.ReadableTextWithLink +import chat.simplex.common.views.helpers.AppBarTitle +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.onboarding.ReadableTextWithLink import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt new file mode 100644 index 0000000000..8542ea52a4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt @@ -0,0 +1,184 @@ +package chat.simplex.common.views.newchat + +import SectionBottomSpacer +import SectionTextFooter +import SectionView +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import dev.icerock.moko.resources.compose.painterResource +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.shareText +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR + +@Composable +fun AddContactView( + chatModel: ChatModel, + connReqInvitation: String, + contactConnection: MutableState<PendingContactConnection?> +) { + val clipboard = LocalClipboardManager.current + AddContactLayout( + chatModel = chatModel, + incognitoPref = chatModel.controller.appPrefs.incognito, + connReq = connReqInvitation, + contactConnection = contactConnection, + share = { clipboard.shareText(connReqInvitation) }, + learnMore = { + ModalManager.center.showModal { + Column( + Modifier + .fillMaxHeight() + .padding(horizontal = DEFAULT_PADDING), + verticalArrangement = Arrangement.SpaceBetween + ) { + AddContactLearnMore() + } + } + } + ) +} + +@Composable +fun AddContactLayout( + chatModel: ChatModel, + incognitoPref: SharedPreference<Boolean>, + connReq: String, + contactConnection: MutableState<PendingContactConnection?>, + share: () -> Unit, + learnMore: () -> Unit +) { + val incognito = remember { mutableStateOf(incognitoPref.get()) } + + LaunchedEffect(incognito.value) { + withApi { + val contactConnVal = contactConnection.value + if (contactConnVal != null) { + chatModel.controller.apiSetConnectionIncognito(contactConnVal.pccConnId, incognito.value)?.let { + contactConnection.value = it + chatModel.updateContactConnection(it) + } + } + } + } + + Column( + Modifier + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween, + ) { + AppBarTitle(stringResource(MR.strings.add_contact)) + + SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) { + if (connReq.isNotEmpty()) { + QRCode( + connReq, Modifier + .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) + .aspectRatio(1f) + ) + } else { + CircularProgressIndicator( + Modifier + .size(36.dp) + .padding(4.dp) + .align(Alignment.CenterHorizontally), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } + + IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } + ShareLinkButton(share) + OneTimeLinkLearnMoreButton(learnMore) + } + SectionTextFooter(sharedProfileInfo(chatModel, incognito.value)) + + SectionBottomSpacer() + } +} + +@Composable +fun ShareLinkButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_share), + stringResource(MR.strings.share_invitation_link), + onClick, + iconColor = MaterialTheme.colors.primary, + textColor = MaterialTheme.colors.primary, + ) +} + +@Composable +fun OneTimeLinkLearnMoreButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_info), + stringResource(MR.strings.learn_more), + onClick, + ) +} + +@Composable +fun IncognitoToggle( + incognitoPref: SharedPreference<Boolean>, + incognito: MutableState<Boolean>, + onClickInfo: () -> Unit +) { + SettingsActionItemWithContent( + icon = if (incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), + text = null, + click = onClickInfo, + iconColor = if (incognito.value) Indigo else MaterialTheme.colors.secondary, + extraPadding = false + ) { + SharedPreferenceToggleWithIcon( + stringResource(MR.strings.incognito), + painterResource(MR.images.ic_info), + stopped = false, + onClickInfo = onClickInfo, + preference = incognitoPref, + preferenceState = incognito + ) + } +} + +fun sharedProfileInfo( + chatModel: ChatModel, + incognito: Boolean +): String { + val name = chatModel.currentUser.value?.displayName ?: "" + return if (incognito) { + generalGetString(MR.strings.connect__a_new_random_profile_will_be_shared) + } else { + String.format(generalGetString(MR.strings.connect__your_profile_will_be_shared), name) + } +} + +@Preview/*( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +)*/ +@Composable +fun PreviewAddContactView() { + SimpleXTheme { + AddContactLayout( + chatModel = ChatModel, + incognitoPref = SharedPreference({ false }, {}), + connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", + contactConnection = mutableStateOf(PendingContactConnection.getSampleData()), + share = {}, + learnMore = {}, + ) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt similarity index 87% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 2609098ebd..3ab5ef9b2b 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -1,6 +1,6 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat -import android.net.Uri +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,25 +15,23 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.ProfileNameField -import chat.simplex.app.views.chat.group.AddGroupMembersView -import chat.simplex.app.views.chatlist.setGroupMembers -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.isValidDisplayName -import chat.simplex.app.views.onboarding.ReadableText -import chat.simplex.app.views.usersettings.DeleteImageButton -import chat.simplex.app.views.usersettings.EditImageButton -import com.google.accompanist.insets.ProvideWindowInsets -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.ProfileNameField +import chat.simplex.common.views.chat.group.AddGroupMembersView +import chat.simplex.common.views.chatlist.setGroupMembers +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.isValidDisplayName +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.usersettings.DeleteImageButton +import chat.simplex.common.views.usersettings.EditImageButton +import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.net.URI @Composable fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { @@ -47,7 +45,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { chatModel.chatId.value = groupInfo.id setGroupMembers(groupInfo, chatModel) close.invoke() - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(groupInfo, true, chatModel, close) } } @@ -63,7 +61,7 @@ fun AddGroupLayout(createGroup: (GroupProfile) -> Unit, close: () -> Unit) { val scope = rememberCoroutineScope() val displayName = rememberSaveable { mutableStateOf("") } val fullName = rememberSaveable { mutableStateOf("") } - val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) } + val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) } val profileImage = rememberSaveable { mutableStateOf<String?>(null) } val focusRequester = remember { FocusRequester() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.kt new file mode 100644 index 0000000000..a2bc2c4df4 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.kt @@ -0,0 +1,11 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.runtime.* +import chat.simplex.common.model.ChatModel + +enum class ConnectViaLinkTab { + SCAN, PASTE +} + +@Composable +expect fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt similarity index 62% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 3317472cc5..934c050d8a 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -1,26 +1,30 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat import SectionBottomSpacer import SectionDividerSpaced +import SectionTextFooter import SectionView -import android.content.res.Configuration +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.LocalAliasEditor -import chat.simplex.app.views.chatlist.deleteContactConnectionAlert -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.SettingsActionItem +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.LocalAliasEditor +import chat.simplex.common.views.chatlist.deleteContactConnectionAlert +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.PendingContactConnection +import chat.simplex.common.platform.shareText +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR @Composable @@ -40,21 +44,22 @@ fun ContactConnectionInfoView( **/ DisposableEffect(Unit) { onDispose { - if (!ModalManager.shared.hasModalsOpen()) { + if (!ModalManager.center.hasModalsOpen()) { chatModel.connReqInv.value = null } } } + val clipboard = LocalClipboardManager.current ContactConnectionInfoLayout( + chatModel = chatModel, connReq = connReqInvitation, - contactConnection, - connIncognito = contactConnection.incognito, - focusAlias, + contactConnection = contactConnection, + focusAlias = focusAlias, deleteConnection = { deleteContactConnectionAlert(contactConnection, chatModel, close) }, onLocalAliasChanged = { setContactAlias(contactConnection, it, chatModel) }, - share = { if (connReqInvitation != null) shareText(connReqInvitation) }, + share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) }, learnMore = { - ModalManager.shared.showModal { + ModalManager.center.showModal { Column( Modifier .fillMaxHeight() @@ -70,22 +75,43 @@ fun ContactConnectionInfoView( @Composable private fun ContactConnectionInfoLayout( + chatModel: ChatModel, connReq: String?, contactConnection: PendingContactConnection, - connIncognito: Boolean, focusAlias: Boolean, deleteConnection: () -> Unit, onLocalAliasChanged: (String) -> Unit, share: () -> Unit, learnMore: () -> Unit, ) { + @Composable fun incognitoEnabled() { + if (contactConnection.incognito) { + SettingsActionItemWithContent( + icon = painterResource(MR.images.ic_theater_comedy_filled), + text = null, + click = { ModalManager.start.showModal { IncognitoView() } }, + iconColor = Indigo, + extraPadding = false + ) { + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(MR.strings.incognito), Modifier.padding(end = 4.dp)) + Icon( + painterResource(MR.images.ic_info), + null, + tint = MaterialTheme.colors.primary + ) + } + } + } + } + Column( Modifier .verticalScroll(rememberScrollState()), ) { AppBarTitle( stringResource( - if (contactConnection.initiated) MR.strings.you_invited_your_contact + if (contactConnection.initiated) MR.strings.you_invited_a_contact else MR.strings.you_accepted_connection ) ) @@ -98,19 +124,27 @@ private fun ContactConnectionInfoLayout( ), Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) ) - OneTimeLinkProfileText(connIncognito) if (contactConnection.groupLinkId == null) { - LocalAliasEditor(contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged) + LocalAliasEditor(contactConnection.id, contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged) } SectionView { if (!connReq.isNullOrEmpty() && contactConnection.initiated) { - OneTimeLinkSection(connReq, share, learnMore) + QRCode( + connReq, Modifier + .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) + .aspectRatio(1f) + ) + incognitoEnabled() + ShareLinkButton(share) + OneTimeLinkLearnMoreButton(learnMore) } else { + incognitoEnabled() OneTimeLinkLearnMoreButton(learnMore) } } + SectionTextFooter(sharedProfileInfo(chatModel, contactConnection.incognito)) SectionDividerSpaced(maxBottomPadding = false) @@ -137,19 +171,18 @@ private fun setContactAlias(contactConnection: PendingContactConnection, localAl } } -@Preview -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable private fun PreviewContactConnectionInfoView() { SimpleXTheme { ContactConnectionInfoLayout( + chatModel = ChatModel, connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", - PendingContactConnection.getSampleData(), - connIncognito = false, + contactConnection = PendingContactConnection.getSampleData(), focusAlias = false, deleteConnection = {}, onLocalAliasChanged = {}, diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt similarity index 63% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt index b0b7b923d7..08afbd4c6c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/CreateLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/CreateLinkView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -9,12 +9,11 @@ import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.helpers.ModalManager -import chat.simplex.app.views.helpers.withApi -import chat.simplex.app.views.usersettings.UserAddressView +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.PendingContactConnection +import chat.simplex.common.views.helpers.ModalManager +import chat.simplex.common.views.helpers.withApi +import chat.simplex.common.views.usersettings.UserAddressView import chat.simplex.res.MR enum class CreateLinkTab { @@ -25,10 +24,16 @@ enum class CreateLinkTab { fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { val selection = remember { mutableStateOf(initialSelection) } val connReqInvitation = rememberSaveable { m.connReqInv } + val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable { mutableStateOf(null) } val creatingConnReq = rememberSaveable { mutableStateOf(false) } LaunchedEffect(selection.value) { - if (selection.value == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && !creatingConnReq.value) { - createInvitation(m, creatingConnReq, connReqInvitation) + if ( + selection.value == CreateLinkTab.ONE_TIME + && connReqInvitation.value.isNullOrEmpty() + && contactConnection.value == null + && !creatingConnReq.value + ) { + createInvitation(m, creatingConnReq, connReqInvitation, contactConnection) } } /** When [AddContactView] is open, we don't need to drop [chatModel.connReqInv]. @@ -37,16 +42,19 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { **/ DisposableEffect(Unit) { onDispose { - if (!ModalManager.shared.hasModalsOpen()) { + if (!ModalManager.center.hasModalsOpen()) { m.connReqInv.value = null } } } val tabTitles = CreateLinkTab.values().map { when { - it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() -> stringResource(MR.strings.create_one_time_link) - it == CreateLinkTab.ONE_TIME -> stringResource(MR.strings.one_time_link) - it == CreateLinkTab.LONG_TERM -> stringResource(MR.strings.your_simplex_contact_address) + it == CreateLinkTab.ONE_TIME && connReqInvitation.value.isNullOrEmpty() && contactConnection.value == null -> + stringResource(MR.strings.create_one_time_link) + it == CreateLinkTab.ONE_TIME -> + stringResource(MR.strings.one_time_link) + it == CreateLinkTab.LONG_TERM -> + stringResource(MR.strings.your_simplex_contact_address) else -> "" } } @@ -58,7 +66,7 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { Column(Modifier.weight(1f)) { when (selection.value) { CreateLinkTab.ONE_TIME -> { - AddContactView(connReqInvitation.value ?: "", m.incognito.value) + AddContactView(m, connReqInvitation.value ?: "", contactConnection) } CreateLinkTab.LONG_TERM -> { UserAddressView(m, viaCreateLinkView = true, close = {}) @@ -91,12 +99,18 @@ fun CreateLinkView(m: ChatModel, initialSelection: CreateLinkTab) { } } -private fun createInvitation(m: ChatModel, creatingConnReq: MutableState<Boolean>, connReqInvitation: MutableState<String?>) { +private fun createInvitation( + m: ChatModel, + creatingConnReq: MutableState<Boolean>, + connReqInvitation: MutableState<String?>, + contactConnection: MutableState<PendingContactConnection?> +) { creatingConnReq.value = true withApi { - val connReq = m.controller.apiAddContact() - if (connReq != null) { - connReqInvitation.value = connReq + val r = m.controller.apiAddContact(incognito = m.controller.appPrefs.incognito.get()) + if (r != null) { + connReqInvitation.value = r.first + contactConnection.value = r.second } else { creatingConnReq.value = false } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt similarity index 88% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index fd7edeaa29..8ec54e344f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -1,8 +1,8 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat -import androidx.activity.compose.BackHandler import androidx.compose.animation.* import androidx.compose.animation.core.* +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -20,14 +20,12 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.core.graphics.ColorUtils -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -42,21 +40,28 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow<AnimatedView stopped, addContact = { closeNewChatSheet(false) - ModalManager.shared.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) } + ModalManager.center.closeModals() + ModalManager.center.showModal { CreateLinkView(chatModel, CreateLinkTab.ONE_TIME) } }, connectViaLink = { closeNewChatSheet(false) - ModalManager.shared.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) } + ModalManager.center.closeModals() + ModalManager.center.showModalCloseable { close -> ConnectViaLinkView(chatModel, close) } }, createGroup = { closeNewChatSheet(false) - ModalManager.shared.showCustomModal { close -> AddGroupView(chatModel, close) } + ModalManager.center.closeModals() + ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, close) } }, closeNewChatSheet, ) } -private val titles = listOf(MR.strings.share_one_time_link, MR.strings.connect_via_link_or_qr, MR.strings.create_group) +private val titles = listOf( + MR.strings.share_one_time_link, + if (appPlatform.isAndroid) MR.strings.connect_via_link_or_qr else MR.strings.connect_via_link, + MR.strings.create_group +) private val icons = listOf(MR.images.ic_add_link, MR.images.ic_qr_code, MR.images.ic_group) @Composable @@ -91,11 +96,13 @@ private fun NewChatSheetLayout( } } } - val maxWidth = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp * density } + val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp + val maxWidth = with(LocalDensity.current) { windowWidth() * density } Column( Modifier .fillMaxSize() - .offset { IntOffset(if (newChat.isGone()) -maxWidth.roundToInt() else 0, 0) } + .padding(end = endPadding) + .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) } .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) } .drawBehind { drawRect(animatedColor.value) }, verticalArrangement = Arrangement.Bottom, @@ -103,7 +110,7 @@ private fun NewChatSheetLayout( ) { val actions = remember { listOf(addContact, connectViaLink, createGroup) } val backgroundColor = if (isInDarkTheme()) - Color(ColorUtils.blendARGB(MaterialTheme.colors.primary.toArgb(), Color.Black.toArgb(), 0.7F)) + blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F) else MaterialTheme.colors.background LazyColumn(Modifier diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt new file mode 100644 index 0000000000..0c13bd4f68 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt @@ -0,0 +1,150 @@ +package chat.simplex.common.views.newchat + +import SectionBottomSpacer +import SectionTextFooter +import androidx.compose.desktop.ui.tooling.preview.Preview +import chat.simplex.common.platform.Log +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.TAG +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.IncognitoView +import chat.simplex.common.views.usersettings.SettingsActionItem +import chat.simplex.res.MR +import java.net.URI +import java.net.URISyntaxException + +@Composable +fun PasteToConnectView(chatModel: ChatModel, close: () -> Unit) { + val connectionLink = remember { mutableStateOf("") } + val clipboard = LocalClipboardManager.current + PasteToConnectLayout( + chatModel = chatModel, + incognitoPref = chatModel.controller.appPrefs.incognito, + connectionLink = connectionLink, + pasteFromClipboard = { + connectionLink.value = clipboard.getText()?.text ?: return@PasteToConnectLayout + }, + close = close + ) +} + +@Composable +fun PasteToConnectLayout( + chatModel: ChatModel, + incognitoPref: SharedPreference<Boolean>, + connectionLink: MutableState<String>, + pasteFromClipboard: () -> Unit, + close: () -> Unit +) { + val incognito = remember { mutableStateOf(incognitoPref.get()) } + + fun connectViaLink(connReqUri: String) { + try { + val uri = URI(connReqUri) + withUriAction(uri) { linkType -> + val action = suspend { + Log.d(TAG, "connectViaUri: connecting") + if (connectViaUri(chatModel, linkType, uri, incognito = incognito.value)) { + close() + } + } + if (linkType == ConnectionLinkType.GROUP) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_via_group_link), + text = generalGetString(MR.strings.you_will_join_group), + confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { action() } } + ) + } else action() + } + } catch (e: RuntimeException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_connection_link), + text = generalGetString(MR.strings.this_string_is_not_a_connection_link) + ) + } catch (e: URISyntaxException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_connection_link), + text = generalGetString(MR.strings.this_string_is_not_a_connection_link) + ) + } + } + + Column( + Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), + verticalArrangement = Arrangement.SpaceBetween, + ) { + AppBarTitle(stringResource(MR.strings.connect_via_link), false) + + Box(Modifier.padding(top = DEFAULT_PADDING, bottom = 6.dp)) { + TextEditor( + connectionLink, + Modifier.height(180.dp), + contentPadding = PaddingValues(), + placeholder = stringResource(MR.strings.paste_the_link_you_received_to_connect_with_your_contact) + ) + } + + if (connectionLink.value == "") { + SettingsActionItem( + painterResource(MR.images.ic_content_paste), + stringResource(MR.strings.paste_button), + click = pasteFromClipboard, + ) + } else { + SettingsActionItem( + painterResource(MR.images.ic_close), + stringResource(MR.strings.clear_verb), + click = { connectionLink.value = "" }, + ) + } + + SettingsActionItem( + painterResource(MR.images.ic_link), + stringResource(MR.strings.connect_button), + click = { connectViaLink(connectionLink.value) }, + ) + + IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } + + SectionTextFooter( + buildAnnotatedString { + append(sharedProfileInfo(chatModel, incognito.value)) + append("\n\n") + append(annotatedStringResource(MR.strings.you_can_also_connect_by_clicking_the_link)) + } + ) + + SectionBottomSpacer() + } +} + + +@Preview/*( + uiMode = Configuration.UI_MODE_NIGHT_YES, + name = "Dark Mode" +)*/ +@Composable +fun PreviewPasteToConnectTextbox() { + SimpleXTheme { + PasteToConnectLayout( + chatModel = ChatModel, + incognitoPref = SharedPreference({ false }, {}), + connectionLink = remember { mutableStateOf("") }, + pasteFromClipboard = {}, + close = {} + ) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt similarity index 62% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/QRCode.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index c3830bd91b..6632925964 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -1,23 +1,21 @@ -package chat.simplex.app.views.newchat +package chat.simplex.common.views.newchat -import android.graphics.Bitmap +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.* +import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.core.graphics.* -import androidx.core.graphics.drawable.toBitmap import boofcv.alg.drawing.FiducialImageEngine import boofcv.alg.fiducial.qrcode.* -import boofcv.android.ConvertBitmap -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.CryptoFile +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.launch @@ -30,24 +28,25 @@ fun QRCode( ) { val scope = rememberCoroutineScope() - BoxWithConstraints { + BoxWithConstraints(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { val maxWidthInPx = with(LocalDensity.current) { maxWidth.roundToPx() } val qr = remember(maxWidthInPx, connReq, tintColor, withLogo) { qrCodeBitmap(connReq, maxWidthInPx).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) .let { if (withLogo) it.addLogo() else it } - .asImageBitmap() } Image( bitmap = qr, contentDescription = stringResource(MR.strings.image_descr_qr_code), - modifier + Modifier + .widthIn(max = 360.dp) + .then(modifier) .clickable { scope.launch { val image = qrCodeBitmap(connReq, 1024).replaceColor(Color.Black.toArgb(), tintColor.toArgb()) .let { if (withLogo) it.addLogo() else it } val file = saveTempImageUncompressed(image, false) if (file != null) { - shareFile("", file.absolutePath) + shareFile("", CryptoFile.plain(file.absolutePath)) } } } @@ -55,7 +54,7 @@ fun QRCode( } } -fun qrCodeBitmap(content: String, size: Int = 1024): Bitmap { +fun qrCodeBitmap(content: String, size: Int = 1024): ImageBitmap { val qrCode = QrCodeEncoder().addAutomatic(content).setError(QrCode.ErrorLevel.L).fixate() /** See [QrCodeGeneratorImage.initialize] and [FiducialImageEngine.configure] for size calculation */ val numModules = QrCode.totalModules(qrCode.version) @@ -69,33 +68,10 @@ fun qrCodeBitmap(content: String, size: Int = 1024): Bitmap { val renderer = QrCodeGeneratorImage(pixelsPerModule + 1) renderer.borderModule = borderModule renderer.render(qrCode) - return ConvertBitmap.grayToBitmap(renderer.gray, Bitmap.Config.RGB_565).scale(size, size) + return renderer.gray.toImageBitmap().scale(size, size) } -fun Bitmap.replaceColor(from: Int, to: Int): Bitmap { - val pixels = IntArray(width * height) - getPixels(pixels, 0, width, 0, 0, width, height) - var i = 0 - while (i < pixels.size) { - if (pixels[i] == from) { - pixels[i] = to - } - i++ - } - setPixels(pixels, 0, width, 0, 0, width, height) - return this -} - -fun Bitmap.addLogo(): Bitmap = applyCanvas { - val radius = (width * 0.16f) / 2 - val paint = android.graphics.Paint() - paint.color = android.graphics.Color.WHITE - drawCircle(width / 2f, height / 2f, radius, paint) - val logo = SimplexApp.context.resources.getDrawable(R.mipmap.icon_foreground, null).toBitmap() - val logoSize = (width * 0.24).toInt() - translate((width - logoSize) / 2f, (height - logoSize) / 2f) - drawBitmap(logo, null, android.graphics.Rect(0, 0, logoSize, logoSize), null) -} +expect fun ImageBitmap.replaceColor(from: Int, to: Int): ImageBitmap @Preview @Composable diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt new file mode 100644 index 0000000000..66ba595e17 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.kt @@ -0,0 +1,6 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.runtime.* + +@Composable +expect fun QRCodeScanner(onBarcode: (String) -> Unit) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt new file mode 100644 index 0000000000..e3fa922755 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt @@ -0,0 +1,165 @@ +package chat.simplex.common.views.newchat + +import SectionBottomSpacer +import SectionTextFooter +import androidx.compose.desktop.ui.tooling.preview.Preview +import chat.simplex.common.platform.Log +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.TAG +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.res.MR +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.net.URI + +@Composable +expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) + +enum class ConnectionLinkType { + CONTACT, INVITATION, GROUP +} + +@Serializable +sealed class CReqClientData { + @Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData() +} + +fun withUriAction(uri: URI, run: suspend (ConnectionLinkType) -> Unit) { + val action = uri.path?.drop(1)?.replace("/", "") + val data = URI(uri.toString().replaceFirst("#/", "/")).getQueryParameter("data") + val type = when { + data != null -> { + val parsed = runCatching { + json.decodeFromString(CReqClientData.serializer(), data) + } + when { + parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP + else -> null + } + } + + action == "contact" -> ConnectionLinkType.CONTACT + action == "invitation" -> ConnectionLinkType.INVITATION + else -> null + } + if (type != null) { + withApi { run(type) } + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_contact_link), + text = generalGetString(MR.strings.this_link_is_not_a_valid_connection_link) + ) + } +} + +suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: URI, incognito: Boolean): Boolean { + val r = chatModel.controller.apiConnect(incognito, uri.toString()) + if (r) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.connection_request_sent), + text = + when (action) { + ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted) + ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online) + ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online) + } + ) + } + return r +} + +@Composable +fun ConnectContactLayout( + chatModel: ChatModel, + incognitoPref: SharedPreference<Boolean>, + close: () -> Unit +) { + val incognito = remember { mutableStateOf(incognitoPref.get()) } + + @Composable + fun QRCodeScanner(close: () -> Unit) { + QRCodeScanner { connReqUri -> + try { + val uri = URI(connReqUri) + withUriAction(uri) { linkType -> + val action = suspend { + Log.d(TAG, "connectViaUri: connecting") + if (connectViaUri(ChatModel, linkType, uri, incognito = incognito.value)) { + close() + } + } + if (linkType == ConnectionLinkType.GROUP) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_via_group_link), + text = generalGetString(MR.strings.you_will_join_group), + confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { action() } } + ) + } else action() + } + } catch (e: RuntimeException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_QR_code), + text = generalGetString(MR.strings.this_QR_code_is_not_a_link) + ) + } + } + } + + Column( + Modifier.verticalScroll(rememberScrollState()).padding(horizontal = DEFAULT_PADDING), + verticalArrangement = Arrangement.SpaceBetween + ) { + AppBarTitle(stringResource(MR.strings.scan_QR_code), false) + Box( + Modifier + .fillMaxWidth() + .aspectRatio(ratio = 1F) + .padding(bottom = 12.dp) + ) { QRCodeScanner(close) } + + IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } + + SectionTextFooter( + buildAnnotatedString { + append(sharedProfileInfo(chatModel, incognito.value)) + append("\n\n") + append(annotatedStringResource(MR.strings.if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link)) + } + ) + + SectionBottomSpacer() + } +} + +fun URI.getQueryParameter(param: String): String? { + if (!query.contains("$param=")) return null + return query.substringAfter("$param=").substringBefore("&") +} + +@Preview/*( + uiMode = Configuration.UI_MODE_NIGHT_YES, + showBackground = true, + name = "Dark Mode" +)*/ +@Composable +fun PreviewConnectContactLayout() { + SimpleXTheme { + ConnectContactLayout( + chatModel = ChatModel, + incognitoPref = SharedPreference({ false }, {}), + close = {}, + ) + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt similarity index 78% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/CreateSimpleXAddress.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index af8b93c0f3..72cbc3a628 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views.onboarding +package chat.simplex.common.views.onboarding import SectionBottomSpacer -import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -9,29 +8,35 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.UserContactLinkRec -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCode +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode import chat.simplex.res.MR @Composable fun CreateSimpleXAddress(m: ChatModel) { var progressIndicator by remember { mutableStateOf(false) } val userAddress = remember { m.userAddress } + val clipboard = LocalClipboardManager.current + val uriHandler = LocalUriHandler.current + + LaunchedEffect(Unit) { + prepareChatBeforeAddressCreation() + } CreateSimpleXAddressLayout( userAddress.value, - share = { address: String -> shareText(address) }, + share = { address: String -> clipboard.shareText(address) }, sendEmail = { address -> - sendEmail( + uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), generalGetString(MR.strings.email_invite_body).format(address.connReqContact) ) @@ -55,8 +60,12 @@ fun CreateSimpleXAddress(m: ChatModel) { } }, nextStep = { - m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step4_SetNotificationsMode) - m.onboardingStage.value = OnboardingStage.Step4_SetNotificationsMode + val next = if (appPlatform.isAndroid) { + OnboardingStage.Step4_SetNotificationsMode + } else { + OnboardingStage.OnboardingComplete + } + m.controller.appPrefs.onboardingStage.set(next) }, ) @@ -165,3 +174,19 @@ private fun ProgressIndicator() { ) } } + +private fun prepareChatBeforeAddressCreation() { + if (chatModel.users.isNotEmpty()) return + withApi { + val user = chatModel.controller.apiGetActiveUser() ?: return@withApi + chatModel.currentUser.value = user + if (chatModel.users.isEmpty()) { + chatModel.controller.startChat(user) + } else { + val users = chatModel.controller.listUsers() + chatModel.users.clear() + chatModel.users.addAll(users) + chatModel.controller.getUserChatData() + } + } +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt similarity index 82% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index c8bc7b5edf..e3dfb2b736 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -1,6 +1,6 @@ -package chat.simplex.app.views.onboarding +package chat.simplex.common.views.onboarding -import android.content.res.Configuration +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -8,22 +8,20 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler +import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.User -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.MarkdownText -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.MarkdownText +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import dev.icerock.moko.resources.compose.stringResource @Composable -fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = null) { +fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) { Column(Modifier .fillMaxWidth() .padding(horizontal = DEFAULT_PADDING), @@ -43,7 +41,7 @@ fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = n if (onboardingStage != null) { Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) { - OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.shared.closeModal() }) + OnboardingActionButton(user, onboardingStage, onclick = { ModalManager.fullscreen.closeModal() }) } Spacer(Modifier.fillMaxHeight().weight(1f)) } @@ -56,7 +54,7 @@ fun ReadableText(stringResId: StringResource, textAlign: TextAlign = TextAlign.S } @Composable -fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp)) { +fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: TextAlign = TextAlign.Start, padding: PaddingValues = PaddingValues(bottom = 12.dp), simplexLink: Boolean = false) { val annotated = annotatedStringResource(stringResId) val primary = MaterialTheme.colors.primary // This replaces links in text highlighted with specific color, e.g. SimplexBlue @@ -72,7 +70,7 @@ fun ReadableTextWithLink(stringResId: StringResource, link: String, textAlign: T newStyles } val uriHandler = LocalUriHandler.current - Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp) + Text(AnnotatedString(annotated.text, newStyles), modifier = Modifier.padding(padding).clickable { if (simplexLink) uriHandler.openVerifiedSimplexUri(link) else uriHandler.openUriCatching(link) }, textAlign = textAlign, lineHeight = 22.sp) } @Composable @@ -87,16 +85,15 @@ fun ReadableMarkdownText(text: String, textAlign: TextAlign = TextAlign.Start, p formattedText = remember(text) { parseToMarkdown(text) }, modifier = Modifier.padding(padding), style = TextStyle(textAlign = textAlign, lineHeight = 22.sp, fontSize = 16.sp), - linkMode = SimplexApp.context.chatModel.controller.appPrefs.simplexLinkMode.get(), + linkMode = ChatController.appPrefs.simplexLinkMode.get(), ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewHowItWorks() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/OnboardingView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt similarity index 80% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/OnboardingView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt index 997d9edb4b..119ed8cd48 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/OnboardingView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/OnboardingView.kt @@ -1,19 +1,20 @@ -package chat.simplex.app.views.onboarding +package chat.simplex.common.views.onboarding import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import chat.simplex.app.model.ChatModel -import chat.simplex.app.views.CreateProfilePanel -import chat.simplex.app.views.helpers.getKeyboardState -import com.google.accompanist.insets.ProvideWindowInsets +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.ProvideWindowInsets +import chat.simplex.common.views.CreateProfilePanel +import chat.simplex.common.platform.getKeyboardState import kotlinx.coroutines.launch enum class OnboardingStage { Step1_SimpleXInfo, Step2_CreateProfile, + Step2_5_SetupDatabasePassphrase, Step3_CreateSimpleXAddress, Step4_SetNotificationsMode, OnboardingComplete diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt similarity index 72% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index c47a983219..aa413016d1 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -1,7 +1,5 @@ -package chat.simplex.app.views.onboarding +package chat.simplex.common.views.onboarding -import android.Manifest -import android.os.Build import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -15,15 +13,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.NtfManager -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.usersettings.NotificationsMode -import chat.simplex.app.views.usersettings.changeNotificationsMode -import com.google.accompanist.permissions.rememberPermissionState +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.NotificationsMode +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.changeNotificationsMode import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource @@ -47,7 +41,7 @@ fun SetNotificationsMode(m: ChatModel) { } Spacer(Modifier.fillMaxHeight().weight(1f)) Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) { - OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) { + OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, false) { changeNotificationsMode(currentMode.value, m) } } @@ -57,23 +51,7 @@ fun SetNotificationsMode(m: ChatModel) { } @Composable -fun SetNotificationsModeAdditions() { - // When target and compile SDK are different - if (Build.VERSION.SDK_INT >= 33 && SimplexApp.context.applicationInfo.targetSdkVersion >= 33) { - val notificationsPermissionState = rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) - LaunchedEffect(notificationsPermissionState.hasPermission) { - if (notificationsPermissionState.hasPermission) { - SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert() - } else { - notificationsPermissionState.launchPermissionRequest() - } - } - } else { - LaunchedEffect(Unit) { - SimplexApp.context.chatModel.controller.ntfManager.createNtfChannelsMaybeShowAlert() - } - } -} +expect fun SetNotificationsModeAdditions() @Composable private fun NotificationButton(currentMode: MutableState<NotificationsMode>, mode: NotificationsMode, title: StringResource, description: StringResource) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt new file mode 100644 index 0000000000..9bc5ae846e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -0,0 +1,233 @@ +package chat.simplex.common.views.onboarding + +import SectionBottomSpacer +import SectionItemView +import SectionItemViewSpaceBetween +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.* +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.* +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.input.ImeAction +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.database.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +@Composable +fun SetupDatabasePassphrase(m: ChatModel) { + val progressIndicator = remember { mutableStateOf(false) } + val prefs = m.controller.appPrefs + val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } + val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } + // Do not do rememberSaveable on current key to prevent saving it on disk in clear text + val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } + val newKey = rememberSaveable { mutableStateOf("") } + val confirmNewKey = rememberSaveable { mutableStateOf("") } + fun nextStep() { + m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress) + } + SetupDatabasePassphraseLayout( + currentKey, + newKey, + confirmNewKey, + progressIndicator, + onConfirmEncrypt = { + withApi { + if (m.chatRunning.value == true) { + // Stop chat if it's started before doing anything + stopChatAsync(m) + } + prefs.storeDBPassphrase.set(false) + + val newKeyValue = newKey.value + val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator) + if (success) { + startChat(newKeyValue) + nextStep() + } else { + // Rollback in case of it is finished with error in order to allow to repeat the process again + prefs.storeDBPassphrase.set(true) + } + } + }, + nextStep = ::nextStep, + ) + + if (progressIndicator.value) { + ProgressIndicator() + } + + DisposableEffect(Unit) { + onDispose { + if (m.chatRunning.value != true) { + withBGApi { + val user = chatController.apiGetActiveUser() + if (user != null) { + m.controller.startChat(user) + } + } + } + } + } +} + +@Composable +private fun SetupDatabasePassphraseLayout( + currentKey: MutableState<String>, + newKey: MutableState<String>, + confirmNewKey: MutableState<String>, + progressIndicator: MutableState<Boolean>, + onConfirmEncrypt: () -> Unit, + nextStep: () -> Unit, +) { + Column( + Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) + + Spacer(Modifier.weight(1f)) + + Column(Modifier.width(600.dp)) { + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + LaunchedEffect(Unit) { + delay(100L) + focusRequester.requestFocus() + } + PassphraseField( + newKey, + generalGetString(MR.strings.new_passphrase), + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .focusRequester(focusRequester) + .onPreviewKeyEvent { + if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) { + focusManager.moveFocus(FocusDirection.Down) + true + } else { + false + } + }, + showStrength = true, + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + encryptDatabaseAlert(onConfirmEncrypt) + } + } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value + + PassphraseField( + confirmNewKey, + generalGetString(MR.strings.confirm_new_passphrase), + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .onPreviewKeyEvent { + if (!disabled && it.key == Key.Enter && it.type == KeyEventType.KeyUp) { + onClickUpdate() + true + } else { + false + } + }, + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { + if (!disabled) onClickUpdate() + defaultKeyboardAction(ImeAction.Done) + }), + ) + + Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { + SetPassphraseButton(disabled, onClickUpdate) + } + + Column { + SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) + SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) + } + } + + Spacer(Modifier.weight(1f)) + SkipButton(progressIndicator.value, nextStep) + + SectionBottomSpacer() + } +} + +@Composable +private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) { + SimpleButtonIconEnded( + stringResource(MR.strings.set_database_passphrase), + painterResource(MR.images.ic_check), + style = MaterialTheme.typography.h2, + color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, + disabled = disabled, + click = onClick + ) +} + +@Composable +private fun SkipButton(disabled: Boolean, onClick: () -> Unit) { + SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color = + if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick) + Text( + stringResource(MR.strings.you_can_change_it_later), + Modifier + .fillMaxWidth() + .padding(horizontal = DEFAULT_PADDING * 3), + style = MaterialTheme.typography.subtitle1, + color = MaterialTheme.colors.secondary, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun ProgressIndicator() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } +} + +private suspend fun startChat(key: String?) { + val m = ChatModel + initChatController(key) + m.chatDbChanged.value = false + m.chatRunning.value = true +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt similarity index 72% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index 9c47931a40..f20c4508b4 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -1,12 +1,11 @@ -package chat.simplex.app.views.onboarding +package chat.simplex.common.views.onboarding -import android.content.res.Configuration +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter @@ -14,14 +13,10 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* -import chat.simplex.app.R -import chat.simplex.app.SimplexApp -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.User -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.ModalManager +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource @@ -29,15 +24,15 @@ import dev.icerock.moko.resources.StringResource fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { SimpleXInfoLayout( user = chatModel.currentUser.value, - onboardingStage = if (onboarding) chatModel.onboardingStage else null, - showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, + onboardingStage = if (onboarding) chatModel.controller.appPrefs.onboardingStage else null, + showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } }, ) } @Composable fun SimpleXInfoLayout( user: User?, - onboardingStage: MutableState<OnboardingStage?>?, + onboardingStage: SharedPreference<OnboardingStage>?, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), ) { Column( @@ -45,6 +40,7 @@ fun SimpleXInfoLayout( .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(start = DEFAULT_PADDING , end = DEFAULT_PADDING, top = DEFAULT_PADDING), + horizontalAlignment = Alignment.CenterHorizontally ) { Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) { SimpleXLogo() @@ -52,9 +48,11 @@ fun SimpleXInfoLayout( Text(stringResource(MR.strings.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 48.dp).padding(horizontal = 36.dp), textAlign = TextAlign.Center) - InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 80.dp) - InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share) - InfoRow(painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized), MR.strings.decentralized, MR.strings.opensource_protocol_and_code_anybody_can_run_servers) + Column { + InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 80.dp) + InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share) + InfoRow(painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized), MR.strings.decentralized, MR.strings.opensource_protocol_and_code_anybody_can_run_servers) + } Spacer(Modifier.fillMaxHeight().weight(1f)) @@ -101,11 +99,11 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour } @Composable -fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) { +fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null) { if (user == null) { - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick) + OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick) } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick) + OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick) } } @@ -113,7 +111,6 @@ fun OnboardingActionButton(user: User?, onboardingStage: MutableState<Onboarding fun OnboardingActionButton( labelId: StringResource, onboarding: OnboardingStage?, - onboardingStage: MutableState<OnboardingStage?>, border: Boolean, onclick: (() -> Unit)? ) { @@ -130,9 +127,8 @@ fun OnboardingActionButton( SimpleButtonFrame(click = { onclick?.invoke() - onboardingStage.value = onboarding if (onboarding != null) { - SimplexApp.context.chatModel.controller.appPrefs.onboardingStage.set(onboarding) + ChatController.appPrefs.onboardingStage.set(onboarding) } }, modifier) { Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp) @@ -143,12 +139,11 @@ fun OnboardingActionButton( } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewSimpleXInfo() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt similarity index 88% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index 8a2b7f1835..390fd0f14f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -1,8 +1,5 @@ -package chat.simplex.app.views.onboarding +package chat.simplex.common.views.onboarding -import android.content.res.Configuration -import android.os.Build -import androidx.annotation.IntegerRes import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,19 +11,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource -import androidx.compose.ui.res.stringResource import dev.icerock.moko.resources.compose.stringResource 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.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.BuildConfig -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @@ -341,12 +334,6 @@ private val versionDescriptions: List<VersionDescription> = listOf( ) ) ), - // Also in v5.1 - // preference to disable calls per contact - // configurable SOCKS proxy port - // access welcome message via a group profile - // improve calls on lock screen - // better formatting of times and dates VersionDescription( version = "v5.1", post = "https://simplex.chat/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.html", @@ -377,7 +364,7 @@ private val versionDescriptions: List<VersionDescription> = listOf( descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) - ), + ) ), VersionDescription( version = "v5.2", @@ -408,8 +395,42 @@ private val versionDescriptions: List<VersionDescription> = listOf( titleId = MR.strings.v5_2_more_things, descrId = MR.strings.v5_2_more_things_descr ) - ), - ) + ) + ), + VersionDescription( + version = "v5.3", + post = "https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html", + features = listOf( + FeatureDescription( + icon = MR.images.ic_desktop, + titleId = MR.strings.v5_3_new_desktop_app, + descrId = MR.strings.v5_3_new_desktop_app_descr, + link = "https://simplex.chat/downloads/" + ), + FeatureDescription( + icon = MR.images.ic_lock, + titleId = MR.strings.v5_3_encrypt_local_files, + descrId = MR.strings.v5_3_encrypt_local_files_descr + ), + FeatureDescription( + icon = MR.images.ic_search, + titleId = MR.strings.v5_3_discover_join_groups, + descrId = MR.strings.v5_3_discover_join_groups_descr, + link = "simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion" + ), + FeatureDescription( + icon = MR.images.ic_theater_comedy, + titleId = MR.strings.v5_3_simpler_incognito_mode, + descrId = MR.strings.v5_3_simpler_incognito_mode_descr + ), + FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_3_new_interface_languages, + descrId = MR.strings.v5_3_new_interface_languages_descr, + link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" + ) + ) + ), ) private val lastVersion = versionDescriptions.last().version @@ -424,12 +445,11 @@ fun shouldShowWhatsNew(m: ChatModel): Boolean { return v != lastVersion } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewWhatsNewView() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt similarity index 96% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index 898f02e883..eedf604a7f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -1,9 +1,10 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionCustomFooter import SectionItemView import SectionView +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -17,14 +18,12 @@ import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel import chat.simplex.res.MR -import dev.icerock.moko.resources.compose.painterResource import java.text.DecimalFormat @Composable @@ -165,9 +164,10 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { ) } SectionItemView { + // can't be higher than 130ms to avoid overflow on 32bit systems TimeoutSettingRow( stringResource(MR.strings.network_option_protocol_timeout_per_kb), networkTCPTimeoutPerKb, - listOf(10_000, 20_000, 40_000, 75_000, 100_000), secondsLabel + listOf(15_000, 30_000, 60_000, 90_000, 120_000), secondsLabel ) } SectionItemView { @@ -254,7 +254,7 @@ fun IntSettingRow(title: String, selection: MutableState<Int>, values: List<Int> Text(title) - ExposedDropdownMenuBox( + DefaultExposedDropdownMenuBox( expanded = expanded.value, onExpandedChange = { expanded.value = !expanded.value @@ -313,7 +313,7 @@ fun TimeoutSettingRow(title: String, selection: MutableState<Long>, values: List Text(title) - ExposedDropdownMenuBox( + DefaultExposedDropdownMenuBox( expanded = expanded.value, onExpandedChange = { expanded.value = !expanded.value @@ -409,7 +409,7 @@ fun showUpdateNetworkSettingsDialog(action: () -> Unit) { ) } -@Preview(showBackground = true) +@Preview @Composable fun PreviewAdvancedNetworkSettingsLayout() { SimpleXTheme { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt new file mode 100644 index 0000000000..75e7d72016 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -0,0 +1,270 @@ +package chat.simplex.common.views.usersettings + +import SectionBottomSpacer +import SectionItemView +import SectionItemViewSpaceBetween +import SectionSpacer +import SectionView +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.* +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* +import chat.simplex.res.MR +import com.godaddy.android.colorpicker.* +import kotlinx.serialization.encodeToString +import java.io.File +import java.net.URI +import java.util.* +import kotlin.collections.ArrayList + +@Composable +expect fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) + +object AppearanceScope { + @Composable + fun ThemesSection( + systemDarkTheme: SharedPreference<String?>, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + editColor: (ThemeColor, Color) -> Unit + ) { + val currentTheme by CurrentColors.collectAsState() + SectionView(stringResource(MR.strings.settings_section_title_themes)) { + val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() + val state = remember { derivedStateOf { currentTheme.name } } + ThemeSelector(state) { + ThemeManager.applyTheme(it, darkTheme) + } + if (state.value == DefaultTheme.SYSTEM.name) { + DarkThemeSelector(remember { systemDarkTheme.state }) { + ThemeManager.changeDarkTheme(it, darkTheme) + } + } + } + SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) } + } + + @Composable + fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + ) { + val currentTheme by CurrentColors.collectAsState() + + AppBarTitle(stringResource(MR.strings.customize_theme_title)) + + SectionView(stringResource(MR.strings.theme_colors_section_title)) { + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { + val title = generalGetString(MR.strings.color_primary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { + val title = generalGetString(MR.strings.color_primary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { + val title = generalGetString(MR.strings.color_secondary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { + val title = generalGetString(MR.strings.color_secondary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { + val title = generalGetString(MR.strings.color_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.background) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { + val title = generalGetString(MR.strings.color_surface) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.surface) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { + val title = generalGetString(MR.strings.color_title) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { + val title = generalGetString(MR.strings.color_sent_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { + val title = generalGetString(MR.strings.color_received_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) + } + } + val isInDarkTheme = isInDarkTheme() + if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { + SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { + Text(generalGetString(MR.strings.reset_color), color = colors.primary) + } + } + SectionSpacer() + SectionView { + val theme = remember { mutableStateOf(null as String?) } + val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val themeValue = theme.value + if (themeValue != null && to != null) { + copyBytesToFile(themeValue.byteInputStream(), to) { + theme.value = null + } + } + } + SectionItemView({ + val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) + theme.value = yaml.encodeToString<ThemeOverrides>(overrides) + withApi { exportThemeLauncher.launch("simplex.theme")} + }) { + Text(generalGetString(MR.strings.export_theme), color = colors.primary) + } + val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val theme = getThemeFromUri(to) + if (theme != null) { + ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + } + } + } + // Can not limit to YAML mime type since it's unsupported by Android + SectionItemView({ withApi { importThemeLauncher.launch("*/*") } }) { + Text(generalGetString(MR.strings.import_theme), color = colors.primary) + } + } + SectionBottomSpacer() + } + } + + @Composable + fun ColorEditor( + name: ThemeColor, + initialColor: Color, + close: () -> Unit, + ) { + Column( + Modifier + .fillMaxWidth() + ) { + AppBarTitle(name.text) + var currentColor by remember { mutableStateOf(initialColor) } + ColorPicker(initialColor) { + currentColor = it + } + + SectionSpacer() + val isInDarkTheme = isInDarkTheme() + TextButton( + onClick = { + ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) + close() + }, + Modifier.align(Alignment.CenterHorizontally), + colors = ButtonDefaults.textButtonColors(contentColor = currentColor) + ) { + Text(generalGetString(MR.strings.save_color)) + } + } + } + + @Composable + fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { + ClassicColorPicker(modifier = Modifier + .fillMaxWidth() + .height(300.dp), + color = HsvColor.from(color = initialColor), showAlphaBar = true, + onColorChanged = { color: HsvColor -> + onColorChanged(color.toColor()) + } + ) + } + + @Composable + fun LangSelector(state: State<String>, onSelected: (String) -> Unit) { + // Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs` + val supportedLanguages = mapOf( + "system" to generalGetString(MR.strings.language_system), + "en" to "English", + "ar" to "العربية", + "bg" to "Български", + "cs" to "Čeština", + "de" to "Deutsch", + "es" to "Español", + "fi" to "Suomi", + "fr" to "Français", + "it" to "Italiano", + "iw" to "עִברִית", + "ja" to "日本語", + "nl" to "Nederlands", + "pl" to "Polski", + "pt-BR" to "Português, Brasil", + "ru" to "Русский", + "th" to "ภาษาไทย", + "uk" to "Українська", + "zh-CN" to "简体中文" + ) + val values by remember(ChatController.appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) } + ExposedDropDownSettingRow( + generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) + } + + @Composable + private fun ThemeSelector(state: State<String>, onSelected: (String) -> Unit) { + val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() + val values by remember(ChatController.appPrefs.appLanguage.state.value) { + mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.theme), + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = onSelected + ) + } + + @Composable + private fun DarkThemeSelector(state: State<String?>, onSelected: (String) -> Unit) { + val values by remember { + val darkThemes = ArrayList<Pair<String, String>>() + darkThemes.add(DefaultTheme.DARK.name to generalGetString(MR.strings.theme_dark)) + darkThemes.add(DefaultTheme.SIMPLEX.name to generalGetString(MR.strings.theme_simplex)) + mutableStateOf(darkThemes.toList()) + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.dark_theme), + values, + state, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { if (it != null) onSelected(it) } + ) + } + //private fun openSystemLangPicker(activity: Activity) { + // activity.startActivity(Intent(Settings.ACTION_APP_LOCALE_SETTINGS, Uri.parse("package:" + SimplexApp.context.packageName))) + //} +} + diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt similarity index 97% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index 38d6a88eb8..d4219a51c5 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionItemView @@ -14,9 +14,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt similarity index 81% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index a3e51c1dab..68aa1fff9e 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionTextFooter @@ -12,10 +12,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.views.TerminalView -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.views.TerminalView +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable @@ -42,3 +42,10 @@ fun DeveloperView( SectionBottomSpacer() } } + +fun showInDevelopingAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.in_developing_title), + text = generalGetString(MR.strings.in_developing_desc) + ) +} diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt similarity index 66% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt index 2a736bf38a..c5bde44947 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/HelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HelpView.kt @@ -1,19 +1,16 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings -import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview -import chat.simplex.app.R -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.chatlist.ChatHelpView -import chat.simplex.app.views.helpers.AppBarTitle +import androidx.compose.desktop.ui.tooling.preview.Preview +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.chatlist.ChatHelpView +import chat.simplex.common.views.helpers.AppBarTitle import chat.simplex.res.MR @Composable @@ -34,12 +31,11 @@ fun HelpLayout(userDisplayName: String) { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewHelpView() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt index 8c266b8975..215899d0ff 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/HiddenProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionItemView @@ -15,13 +15,12 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.User -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chatlist.UserProfileRow -import chat.simplex.app.views.database.PassphraseField -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.User +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.UserProfileRow +import chat.simplex.common.views.database.PassphraseField +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt similarity index 76% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt index b7aeb0a802..e264172f9c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/IncognitoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/IncognitoView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import androidx.compose.foundation.* @@ -8,10 +8,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.helpers.AppBarTitle -import chat.simplex.app.views.helpers.generalGetString +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.AppBarTitle +import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @Composable @@ -32,7 +31,6 @@ fun IncognitoLayout() { Text(generalGetString(MR.strings.incognito_info_protects)) Text(generalGetString(MR.strings.incognito_info_allows)) Text(generalGetString(MR.strings.incognito_info_share)) - Text(generalGetString(MR.strings.incognito_info_find)) SectionBottomSpacer() } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/MarkdownHelpView.kt similarity index 84% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/MarkdownHelpView.kt index aa405ef15c..ab2df2014c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/MarkdownHelpView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/MarkdownHelpView.kt @@ -1,23 +1,19 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer -import android.content.res.Configuration import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.Format -import chat.simplex.app.model.FormatColor -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.common.model.Format +import chat.simplex.common.model.FormatColor +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.res.MR @Composable @@ -87,12 +83,11 @@ fun appendColor(b: AnnotatedString.Builder, s: String, c: FormatColor, after: St b.append(after) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewMarkdownHelpView() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 72292d80ba..f2fee926a7 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionCustomFooter @@ -20,14 +20,16 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.* import androidx.compose.ui.text.input.* -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.ClickableText -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ClickableText +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.annotatedStringResource import chat.simplex.res.MR @Composable @@ -178,7 +180,13 @@ fun NetworkAndServersView( SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), showSettingsModal { AdvancedNetworkSettingsView(it) }) } if (networkUseSocksProxy.value) { - SectionCustomFooter { Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) } + SectionCustomFooter { + Column { + Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) + } + } Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) } else { Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) @@ -336,9 +344,9 @@ private fun UseOnionHosts( val values = remember { OnionHosts.values().map { when (it) { - OnionHosts.NEVER -> ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), generalGetString(MR.strings.network_use_onion_hosts_no_desc)) - OnionHosts.PREFER -> ValueTitleDesc(OnionHosts.PREFER, generalGetString(MR.strings.network_use_onion_hosts_prefer), generalGetString(MR.strings.network_use_onion_hosts_prefer_desc)) - OnionHosts.REQUIRED -> ValueTitleDesc(OnionHosts.REQUIRED, generalGetString(MR.strings.network_use_onion_hosts_required), generalGetString(MR.strings.network_use_onion_hosts_required_desc)) + OnionHosts.NEVER -> ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc))) + OnionHosts.PREFER -> ValueTitleDesc(OnionHosts.PREFER, generalGetString(MR.strings.network_use_onion_hosts_prefer), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_prefer_desc))) + OnionHosts.REQUIRED -> ValueTitleDesc(OnionHosts.REQUIRED, generalGetString(MR.strings.network_use_onion_hosts_required), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_required_desc))) } } } @@ -367,11 +375,12 @@ private fun SessionModePicker( showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), updateSessionMode: (TransportSessionMode) -> Unit, ) { + val density = LocalDensity.current val values = remember { TransportSessionMode.values().map { when (it) { - TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(MR.strings.network_session_mode_user), generalGetString(MR.strings.network_session_mode_user_description)) - TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(MR.strings.network_session_mode_entity), generalGetString(MR.strings.network_session_mode_entity_description)) + TransportSessionMode.User -> ValueTitleDesc(TransportSessionMode.User, generalGetString(MR.strings.network_session_mode_user), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_user_description), density)) + TransportSessionMode.Entity -> ValueTitleDesc(TransportSessionMode.Entity, generalGetString(MR.strings.network_session_mode_entity), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_session_mode_entity_description), density)) } } } @@ -434,7 +443,7 @@ private fun showUpdateNetworkSettingsDialog( ) } -@Preview(showBackground = true) +@Preview @Composable fun PreviewNetworkAndServersLayout() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt similarity index 61% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 054686b0de..ddd1b4068e 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -1,46 +1,25 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionView import SectionViewSelectable -import android.os.Build import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.capitalize import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow -import chat.simplex.app.* -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.coroutines.* import kotlin.collections.ArrayList -enum class NotificationsMode(private val requiresIgnoringBatterySinceSdk: Int) { - OFF(Int.MAX_VALUE), PERIODIC(Build.VERSION_CODES.M), SERVICE(Build.VERSION_CODES.S), /*INSTANT(Int.MAX_VALUE) - for Firebase notifications */; - - val requiresIgnoringBattery - get() = requiresIgnoringBatterySinceSdk <= Build.VERSION.SDK_INT - - companion object { - val default: NotificationsMode = SERVICE - } -} - -enum class NotificationPreviewMode { - MESSAGE, CONTACT, HIDDEN; - - companion object { - val default: NotificationPreviewMode = MESSAGE - } -} - @Composable fun NotificationsSettingsView( chatModel: ChatModel, @@ -51,14 +30,14 @@ fun NotificationsSettingsView( } NotificationsSettingsLayout( - notificationsMode = chatModel.notificationsMode, + notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state }, notificationPreviewMode = chatModel.notificationPreviewMode, showPage = { page -> - ModalManager.shared.showModalCloseable(true) { - when (page) { - CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.notificationsMode) { changeNotificationsMode(it, chatModel) } - CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected) - } + ModalManager.start.showModalCloseable(true) { + when (page) { + CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) } + CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected) + } } }, ) @@ -82,13 +61,15 @@ fun NotificationsSettingsLayout( ) { AppBarTitle(stringResource(MR.strings.notifications)) SectionView(null) { - SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) { - Text( - modes.first { it.value == notificationsMode.value }.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) + if (appPlatform == AppPlatform.ANDROID) { + SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) { + Text( + modes.first { it.value == notificationsMode.value }.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.secondary + ) + } } SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) { Text( @@ -138,21 +119,21 @@ private fun notificationModes(): List<ValueTitleDesc<NotificationsMode>> { ValueTitleDesc( NotificationsMode.OFF, generalGetString(MR.strings.notifications_mode_off), - generalGetString(MR.strings.notifications_mode_off_desc), + AnnotatedString(generalGetString(MR.strings.notifications_mode_off_desc)), ) ) res.add( ValueTitleDesc( NotificationsMode.PERIODIC, generalGetString(MR.strings.notifications_mode_periodic), - generalGetString(MR.strings.notifications_mode_periodic_desc), + AnnotatedString(generalGetString(MR.strings.notifications_mode_periodic_desc)), ) ) res.add( ValueTitleDesc( NotificationsMode.SERVICE, generalGetString(MR.strings.notifications_mode_service), - generalGetString(MR.strings.notifications_mode_service_desc), + AnnotatedString(generalGetString(MR.strings.notifications_mode_service_desc)), ) ) return res @@ -165,42 +146,27 @@ fun notificationPreviewModes(): List<ValueTitleDesc<NotificationPreviewMode>> { ValueTitleDesc( NotificationPreviewMode.MESSAGE, generalGetString(MR.strings.notification_preview_mode_message), - generalGetString(MR.strings.notification_preview_mode_message_desc), + AnnotatedString(generalGetString(MR.strings.notification_preview_mode_message_desc)), ) ) res.add( ValueTitleDesc( NotificationPreviewMode.CONTACT, generalGetString(MR.strings.notification_preview_mode_contact), - generalGetString(MR.strings.notification_preview_mode_contact_desc), + AnnotatedString(generalGetString(MR.strings.notification_preview_mode_contact_desc)), ) ) res.add( ValueTitleDesc( NotificationPreviewMode.HIDDEN, generalGetString(MR.strings.notification_preview_mode_hidden), - generalGetString(MR.strings.notification_display_mode_hidden_desc), + AnnotatedString(generalGetString(MR.strings.notification_display_mode_hidden_desc)), ) ) return res } fun changeNotificationsMode(mode: NotificationsMode, chatModel: ChatModel) { - chatModel.controller.appPrefs.notificationsMode.set(mode.name) - if (mode.requiresIgnoringBattery && !SimplexService.isIgnoringBatteryOptimizations()) { - chatModel.controller.appPrefs.backgroundServiceNoticeShown.set(false) - } - chatModel.notificationsMode.value = mode - SimplexService.StartReceiver.toggleReceiver(mode == NotificationsMode.SERVICE) - CoroutineScope(Dispatchers.Default).launch { - if (mode == NotificationsMode.SERVICE) - SimplexService.start() - else - SimplexService.safeStopService(SimplexApp.context) - } - - if (mode != NotificationsMode.PERIODIC) { - MessagesFetcherWorker.cancelAll() - } - SimplexService.showBackgroundServiceNoticeIfNeeded() + chatModel.controller.appPrefs.notificationsMode.set(mode) + platform.androidNotificationsModeChanged(mode) } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt similarity index 94% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index c6da840f2f..202602b287 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced @@ -13,9 +13,8 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* import chat.simplex.res.MR @Composable @@ -26,9 +25,11 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { fun savePrefs(afterSave: () -> Unit = {}) { withApi { val newProfile = user.profile.toProfile().copy(preferences = preferences.toPreferences()) - val updatedProfile = m.controller.apiUpdateProfile(newProfile) - if (updatedProfile != null) { + val updated = m.controller.apiUpdateProfile(newProfile) + if (updated != null) { + val (updatedProfile, updatedContacts) = updated m.updateCurrentUser(updatedProfile, preferences) + updatedContacts.forEach(m::updateContact) currentPreferences = preferences } afterSave() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt similarity index 72% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 899c4cd516..ef0940b2a0 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -1,11 +1,10 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -import android.view.WindowManager import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -13,7 +12,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -21,19 +19,19 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.fragment.app.FragmentActivity -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.ProfileNameField -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.helpers.DatabaseUtils.ksAppPassword -import chat.simplex.app.views.helpers.DatabaseUtils.ksSelfDestructPassword -import chat.simplex.app.views.isValidDisplayName -import chat.simplex.app.views.localauth.SetAppPasscodeView -import chat.simplex.app.views.onboarding.ReadableText import chat.simplex.res.MR -import kotlinx.coroutines.runBlocking +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.ProfileNameField +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.helpers.DatabaseUtils.ksAppPassword +import chat.simplex.common.views.helpers.DatabaseUtils.ksSelfDestructPassword +import chat.simplex.common.views.isValidDisplayName +import chat.simplex.common.views.localauth.SetAppPasscodeView +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.AppPlatform +import chat.simplex.common.platform.appPlatform enum class LAMode { SYSTEM, @@ -44,6 +42,11 @@ enum class LAMode { SYSTEM -> generalGetString(MR.strings.la_mode_system) PASSCODE -> generalGetString(MR.strings.la_mode_passcode) } + + companion object { + val default: LAMode + get() = if (appPlatform == AppPlatform.ANDROID) SYSTEM else PASSCODE + } } @Composable @@ -57,25 +60,31 @@ fun PrivacySettingsView( ) { val simplexLinkMode = chatModel.controller.appPrefs.simplexLinkMode AppBarTitle(stringResource(MR.strings.your_privacy)) - SectionView(stringResource(MR.strings.settings_section_title_device)) { - ChatLockItem(chatModel, showSettingsModal, setPerformLA) - val context = LocalContext.current - SettingsPreferenceItem(painterResource(MR.images.ic_visibility_off), stringResource(MR.strings.protect_app_screen), chatModel.controller.appPrefs.privacyProtectScreen) { on -> - if (on) { - (context as? FragmentActivity)?.window?.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE - ) - } else { - (context as? FragmentActivity)?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } - } + PrivacyDeviceSection(showSettingsModal, setPerformLA) SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles) SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) + SettingsPreferenceItem( + painterResource(MR.images.ic_chat_bubble), + stringResource(MR.strings.privacy_show_last_messages), + chatModel.controller.appPrefs.privacyShowChatPreviews, + onChange = { showPreviews -> + chatModel.showChatPreviews.value = showPreviews + } + ) + SettingsPreferenceItem( + painterResource(MR.images.ic_edit_note), + stringResource(MR.strings.privacy_message_draft), + chatModel.controller.appPrefs.privacySaveLastDraft, + onChange = { saveDraft -> + if (!saveDraft) { + chatModel.draft.value = null + chatModel.draftChatId.value = null + } + }) SimpleXLinkOptions(chatModel.simplexLinkMode, onSelected = { simplexLinkMode.set(it) chatModel.simplexLinkMode.value = it @@ -111,6 +120,29 @@ fun PrivacySettingsView( } } + fun setSendReceiptsGroups(enable: Boolean, clearOverrides: Boolean) { + withApi { + val mrs = UserMsgReceiptSettings(enable, clearOverrides) + chatModel.controller.apiSetUserGroupReceipts(currentUser.userId, mrs) + chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) + chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) + if (clearOverrides) { + // For loop here is to prevent ConcurrentModificationException that happens with forEach + for (i in 0 until chatModel.chats.size) { + val chat = chatModel.chats[i] + if (chat.chatInfo is ChatInfo.Group) { + var groupInfo = chat.chatInfo.groupInfo + val sendRcpts = groupInfo.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) + chatModel.updateGroup(groupInfo) + } + } + } + } + } + } + DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> @@ -127,6 +159,21 @@ fun PrivacySettingsView( } else { showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) } + }, + setOrAskSendReceiptsGroups = { enable -> + val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Group) { + val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (groupReceiptsOverrides == 0) { + setSendReceiptsGroups(enable, clearOverrides = false) + } else { + showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) + } } ) } @@ -155,10 +202,17 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State<SimplexLinkMode>, onS ) } +@Composable +expect fun PrivacyDeviceSection( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + setPerformLA: (Boolean) -> Unit, +) + @Composable private fun DeliveryReceiptsSection( currentUser: User, setOrAskSendReceiptsContacts: (Boolean) -> Unit, + setOrAskSendReceiptsGroups: (Boolean) -> Unit, ) { SectionView(stringResource(MR.strings.settings_section_title_delivery_receipts)) { SettingsActionItemWithContent(painterResource(MR.images.ic_person), stringResource(MR.strings.receipts_section_contacts)) { @@ -169,6 +223,14 @@ private fun DeliveryReceiptsSection( } ) } + SettingsActionItemWithContent(painterResource(MR.images.ic_group), stringResource(MR.strings.receipts_section_groups)) { + DefaultSwitch( + checked = currentUser.sendRcptsSmallGroups ?: false, + onCheckedChange = { enable -> + setOrAskSendReceiptsGroups(enable) + } + ) + } } SectionTextFooter( remember(currentUser.displayName) { @@ -219,6 +281,41 @@ private fun showUserContactsReceiptsAlert( ) } +private fun showUserGroupsReceiptsAlert( + enable: Boolean, + groupReceiptsOverrides: Int, + setSendReceiptsGroups: (Boolean, Boolean) -> Unit +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(if (enable) MR.strings.receipts_groups_title_enable else MR.strings.receipts_groups_title_disable), + text = AnnotatedString(String.format(generalGetString(if (enable) MR.strings.receipts_groups_override_disabled else MR.strings.receipts_groups_override_enabled), groupReceiptsOverrides)), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + setSendReceiptsGroups(enable, false) + }) { + val t = stringResource(if (enable) MR.strings.receipts_groups_enable_keep_overrides else MR.strings.receipts_groups_disable_keep_overrides) + Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + setSendReceiptsGroups(enable, true) + } + ) { + val t = stringResource(if (enable) MR.strings.receipts_groups_enable_for_all else MR.strings.receipts_groups_disable_for_all) + Text(t, Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.onBackground) + } + } + } + ) +} + private val laDelays = listOf(10, 30, 60, 180, 0) @Composable @@ -231,7 +328,6 @@ fun SimplexLockView( val laMode = remember { chatModel.controller.appPrefs.laMode.state } val laLockDelay = remember { chatModel.controller.appPrefs.laLockDelay } val showChangePasscode = remember { derivedStateOf { performLA.value && currentLAMode.state.value == LAMode.PASSCODE } } - val activity = LocalContext.current as FragmentActivity val selfDestructPref = remember { chatModel.controller.appPrefs.selfDestruct } val selfDestructDisplayName = remember { mutableStateOf(chatModel.controller.appPrefs.selfDestructDisplayName.get() ?: "") } val selfDestructDisplayNamePref = remember { chatModel.controller.appPrefs.selfDestructDisplayName } @@ -243,7 +339,7 @@ fun SimplexLockView( fun disableUnavailableLA() { resetLAEnabled(false) - currentLAMode.set(LAMode.SYSTEM) + currentLAMode.set(LAMode.default) laUnavailableInstructionAlert() } @@ -283,7 +379,7 @@ fun SimplexLockView( } } LAMode.PASSCODE -> { - ModalManager.shared.showCustomModal { close -> + ModalManager.fullscreen.showCustomModal { close -> Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { SetAppPasscodeView( submit = { @@ -311,7 +407,7 @@ fun SimplexLockView( is LAResult.Failed -> { /* Can be called multiple times on every failure */ } LAResult.Success -> { if (!selfDestruct.get()) { - ModalManager.shared.showCustomModal { close -> + ModalManager.fullscreen.showCustomModal { close -> EnableSelfDestruct(selfDestruct, close) } } else { @@ -327,7 +423,7 @@ fun SimplexLockView( authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.la_change_app_passcode)) { laResult -> when (laResult) { LAResult.Success -> { - ModalManager.shared.showCustomModal { close -> + ModalManager.fullscreen.showCustomModal { close -> Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { SetAppPasscodeView( submit = { @@ -350,7 +446,7 @@ fun SimplexLockView( authenticate(generalGetString(MR.strings.la_current_app_passcode), generalGetString(MR.strings.change_self_destruct_passcode)) { laResult -> when (laResult) { LAResult.Success -> { - ModalManager.shared.showCustomModal { close -> + ModalManager.fullscreen.showCustomModal { close -> Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { SetAppPasscodeView( passcodeKeychain = ksSelfDestructPassword, @@ -385,7 +481,7 @@ fun SimplexLockView( setPerformLA(true) } LAMode.PASSCODE -> { - ModalManager.shared.showCustomModal { close -> + ModalManager.fullscreen.showCustomModal { close -> Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { SetAppPasscodeView( submit = { @@ -406,12 +502,14 @@ fun SimplexLockView( setPerformLA(false) } } - LockModeSelector(laMode) { newLAMode -> - if (laMode.value == newLAMode) return@LockModeSelector - if (chatModel.controller.appPrefs.performLA.get()) { - toggleLAMode(newLAMode) - } else { - currentLAMode.set(newLAMode) + if (appPlatform == AppPlatform.ANDROID) { + LockModeSelector(laMode) { newLAMode -> + if (laMode.value == newLAMode) return@LockModeSelector + if (chatModel.controller.appPrefs.performLA.get()) { + toggleLAMode(newLAMode) + } else { + currentLAMode.set(newLAMode) + } } } @@ -430,7 +528,7 @@ fun SimplexLockView( SectionDividerSpaced() SectionView(stringResource(MR.strings.self_destruct_passcode).uppercase()) { val openInfo = { - ModalManager.shared.showModal { + ModalManager.start.showModal { SelfDestructInfoView() } } @@ -584,3 +682,30 @@ private fun passcodeAlert(title: String) { private fun selfDestructPasscodeAlert(title: String) { AlertManager.shared.showAlertMsg(title, generalGetString(MR.strings.if_you_enter_passcode_data_removed)) } + +fun laTurnedOnAlert() = AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.auth_simplex_lock_turned_on), + generalGetString(MR.strings.auth_you_will_be_required_to_authenticate_when_you_start_or_resume) +) + +fun laPasscodeNotSetAlert() = AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.lock_not_enabled), + generalGetString(MR.strings.you_can_turn_on_lock) +) + +fun laFailedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.la_auth_failed), + text = generalGetString(MR.strings.la_could_not_be_verified) + ) +} + +fun laUnavailableInstructionAlert() = AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.auth_unavailable), + generalGetString(MR.strings.auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled) +) + +fun laUnavailableTurningOffAlert() = AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.auth_unavailable), + generalGetString(MR.strings.auth_device_authentication_is_disabled_turning_off) +) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt similarity index 93% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt index a711275ccf..f3896a3de9 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt @@ -1,11 +1,11 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween import SectionView -import android.util.Log +import chat.simplex.common.platform.Log import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer @@ -20,15 +20,14 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.model.* -import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCode +import chat.simplex.common.platform.TAG +import chat.simplex.common.model.* +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.model.ChatModel import chat.simplex.res.MR -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.isActive import kotlinx.coroutines.launch diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt index b7e3e2a202..92246b72fb 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced @@ -19,11 +19,11 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.views.usersettings.ScanProtocolServer import chat.simplex.res.MR import kotlinx.coroutines.launch @@ -62,7 +62,7 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () } fun showServer(server: ServerCfg) { - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.start.showModalCloseable(true) { close -> var old by remember { mutableStateOf(server) } val index = servers.indexOf(old) ProtocolServerView( @@ -114,18 +114,20 @@ fun ProtocolServersView(m: ChatModel, serverProtocol: ServerProtocol, close: () }) { Text(stringResource(MR.strings.smp_servers_enter_manually), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } - SectionItemView({ - AlertManager.shared.hideAlert() - ModalManager.shared.showModalCloseable { close -> - ScanProtocolServer { - close() - servers = servers + it - m.userSMPServersUnsaved.value = servers + if (appPlatform.isAndroid) { + SectionItemView({ + AlertManager.shared.hideAlert() + ModalManager.start.showModalCloseable { close -> + ScanProtocolServer { + close() + servers = servers + it + m.userSMPServersUnsaved.value = servers + } } } - } - ) { - Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + ) { + Text(stringResource(MR.strings.smp_servers_scan_qr), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } val hasAllPresets = hasAllPresets(presetServers, servers, m) if (!hasAllPresets) { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt similarity index 96% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt index f79b968b15..7cb30440d3 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/RTCServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/RTCServers.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionItemViewSpaceBetween @@ -18,11 +18,10 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.call.parseRTCIceServers -import chat.simplex.app.views.helpers.* +import chat.simplex.common.model.ChatModel +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.call.parseRTCIceServers +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt similarity index 54% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt index 69f1e31435..02582ec93c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt @@ -1,32 +1,22 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings -import android.Manifest import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.model.ServerAddress.Companion.parseServerAddress -import chat.simplex.app.model.ServerCfg -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCodeScanner -import com.google.accompanist.permissions.rememberPermissionState +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.model.ServerCfg +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCodeScanner import chat.simplex.res.MR @Composable -fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) { - val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) - LaunchedEffect(Unit) { - cameraPermissionState.launchPermissionRequest() - } - ScanProtocolServerLayout(onNext) -} +expect fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) @Composable -private fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) { +fun ScanProtocolServerLayout(onNext: (ServerCfg) -> Unit) { Column( Modifier .fillMaxSize() diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt similarity index 91% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/SetDeliveryReceiptsView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index 04fe102ddd..089ec7713f 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/SetDeliveryReceiptsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt @@ -1,7 +1,6 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer -import android.util.Log import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -12,11 +11,12 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign -import chat.simplex.app.TAG -import chat.simplex.app.model.ChatModel -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.helpers.* +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.* import chat.simplex.res.MR +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.* @Composable fun SetDeliveryReceiptsView(m: ChatModel) { @@ -73,8 +73,9 @@ private fun SetDeliveryReceiptsLayout( skip: () -> Unit, userCount: Int, ) { + val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING), + Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING, end = endPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt similarity index 73% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index b033f69f38..8969e48b2c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced @@ -6,7 +6,7 @@ import SectionItemView import SectionItemViewWithIcon import SectionView import TextIconSpaced -import android.content.res.Configuration +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -21,57 +21,51 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.* -import androidx.fragment.app.FragmentActivity -import androidx.work.WorkManager -import chat.simplex.app.* -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.database.DatabaseView -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.SimpleXInfo -import chat.simplex.app.views.onboarding.WhatsNewView +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.database.DatabaseView +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.SimpleXInfo +import chat.simplex.common.views.onboarding.WhatsNewView import chat.simplex.res.MR -import com.jakewharton.processphoenix.ProcessPhoenix +import kotlinx.coroutines.launch @Composable -fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { +fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerState: DrawerState) { val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false - MaintainIncognitoState(chatModel) - if (user != null) { val requireAuth = remember { chatModel.controller.appPrefs.performLA.state } SettingsLayout( profile = user.profile, stopped, chatModel.chatDbEncrypted.value == true, - chatModel.incognito, - chatModel.controller.appPrefs.incognito, + remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, + remember { chatModel.controller.appPrefs.notificationsMode.state }, user.displayName, setPerformLA = setPerformLA, - showModal = { modalView -> { ModalManager.shared.showModal { modalView(chatModel) } } }, - showSettingsModal = { modalView -> { ModalManager.shared.showModal(true) { modalView(chatModel) } } }, + showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, + showSettingsModal = { modalView -> { ModalManager.start.showModal(true) { modalView(chatModel) } } }, showSettingsModalWithSearch = { modalView -> - ModalManager.shared.showCustomModal { close -> + ModalManager.start.showCustomModal { close -> val search = rememberSaveable { mutableStateOf("") } ModalView( { close() }, endButtons = { - SearchTextField(Modifier.fillMaxWidth(), alwaysVisible = true) { search.value = it } + SearchTextField(Modifier.fillMaxWidth(), placeholder = stringResource(MR.strings.search_verb), alwaysVisible = true) { search.value = it } }, content = { modalView(chatModel, search) }) } }, - showCustomModal = { modalView -> { ModalManager.shared.showCustomModal { close -> modalView(chatModel, close) } } }, + showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, showVersion = { withApi { val info = chatModel.controller.apiGetVersion() if (info != null) { - ModalManager.shared.showModal { VersionInfoView(info) } + ModalManager.start.showModal { VersionInfoView(info) } } } }, @@ -80,7 +74,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { block() } else { var autoShow = true - ModalManager.shared.showModalCloseable { close -> + ModalManager.fullscreen.showModalCloseable { close -> val onFinishAuth = { success: Boolean -> if (success) { close() @@ -109,6 +103,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit) { } } }, + drawerState = drawerState, ) } } @@ -121,8 +116,8 @@ fun SettingsLayout( profile: LocalProfile, stopped: Boolean, encrypted: Boolean, - incognito: MutableState<Boolean>, - incognitoPref: SharedPreference<Boolean>, + passphraseSaved: Boolean, + notificationsMode: State<NotificationsMode>, userDisplayName: String, setPerformLA: (Boolean) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -130,15 +125,25 @@ fun SettingsLayout( showSettingsModalWithSearch: (@Composable (ChatModel, MutableState<String>) -> Unit) -> Unit, showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, + drawerState: DrawerState, ) { + val scope = rememberCoroutineScope() + val closeSettings: () -> Unit = { scope.launch { drawerState.close() } } + if (drawerState.isOpen) { + BackHandler { + closeSettings() + } + } val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - Box(Modifier.fillMaxSize().verticalScroll(rememberScrollState()).themedBackground(theme.value.base)) { + Box(Modifier.fillMaxSize()) { Column( Modifier .fillMaxSize() - .padding(top = DEFAULT_PADDING) + .verticalScroll(rememberScrollState()) + .themedBackground(theme.value.base) + .padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 3) ) { AppBarTitle(stringResource(MR.strings.your_settings)) @@ -148,19 +153,18 @@ fun SettingsLayout( } val profileHidden = rememberSaveable { mutableStateOf(false) } SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden) } } }, disabled = stopped, extraPadding = true) - SettingsIncognitoActionItem(incognitoPref, incognito, stopped) { showModal { IncognitoView() }() } SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) ChatPreferencesItem(showCustomModal, stopped = stopped) } SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_settings)) { - SettingsActionItem(painterResource(MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView(it, showModal, showSettingsModal, showCustomModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) - DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } SectionDividerSpaced() @@ -168,7 +172,7 @@ fun SettingsLayout( SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openUriCatching(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) } SectionDividerSpaced() @@ -180,55 +184,32 @@ fun SettingsLayout( } SectionDividerSpaced() - SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) - AppVersionItem(showVersion) - } + SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) SectionBottomSpacer() } - } -} - -@Composable -fun SettingsIncognitoActionItem( - incognitoPref: SharedPreference<Boolean>, - incognito: MutableState<Boolean>, - stopped: Boolean, - onClickInfo: () -> Unit, -) { - SettingsPreferenceItemWithInfo( - if (incognito.value) painterResource(MR.images.ic_theater_comedy_filled) else painterResource(MR.images.ic_theater_comedy), - if (incognito.value) Indigo else MaterialTheme.colors.secondary, - stringResource(MR.strings.incognito), - stopped, - onClickInfo, - incognitoPref, - incognito - ) -} - -@Composable -fun MaintainIncognitoState(chatModel: ChatModel) { - // Cache previous value and once it changes in background, update it via API - var cachedIncognito by remember { mutableStateOf(chatModel.incognito.value) } - LaunchedEffect(chatModel.incognito.value) { - // Don't do anything if nothing changed - if (cachedIncognito == chatModel.incognito.value) return@LaunchedEffect - try { - chatModel.controller.apiSetIncognito(chatModel.incognito.value) - } catch (e: Exception) { - // Rollback the state - chatModel.controller.appPrefs.incognito.set(cachedIncognito) - // Crash the app - throw e + if (appPlatform.isDesktop) { + Box( + Modifier + .fillMaxWidth() + .background(MaterialTheme.colors.background) + .background(if (isInDarkTheme()) ToolbarDark else ToolbarLight) + .padding(start = 4.dp, top = 8.dp) + ) { + NavigationButtonBack(closeSettings) + } } - cachedIncognito = chatModel.incognito.value } } -@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { +@Composable +expect fun SettingsSectionApp( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), + showVersion: () -> Unit, + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +) + +@Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { SectionItemViewWithIcon(openDatabaseView) { Row( Modifier.fillMaxWidth(), @@ -238,7 +219,7 @@ fun MaintainIncognitoState(chatModel: ChatModel) { Icon( painterResource(MR.images.ic_database), contentDescription = stringResource(MR.strings.database_passphrase_and_export), - tint = if (encrypted) MaterialTheme.colors.secondary else WarningOrange, + tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange, ) TextIconSpaced(true) Text(stringResource(MR.strings.database_passphrase_and_export)) @@ -273,14 +254,13 @@ fun MaintainIncognitoState(chatModel: ChatModel) { @Composable fun ChatLockItem( - chatModel: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), setPerformLA: (Boolean) -> Unit ) { - val performLA = remember { chatModel.performLA } - val currentLAMode = remember { chatModel.controller.appPrefs.laMode } + val performLA = remember { ChatModel.performLA } + val currentLAMode = remember { ChatModel.controller.appPrefs.laMode } SettingsActionItemWithContent( - click = showSettingsModal { SimplexLockView(chatModel, currentLAMode, setPerformLA) }, + click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) }, icon = if (performLA.value) painterResource(MR.images.ic_lock_filled) else painterResource(MR.images.ic_lock), text = stringResource(MR.strings.chat_lock), iconColor = if (performLA.value) SimplexGreen else MaterialTheme.colors.secondary, @@ -354,12 +334,13 @@ fun ChatLockItem( } } -@Composable private fun AppVersionItem(showVersion: () -> Unit) { +@Composable +fun AppVersionItem(showVersion: () -> Unit) { SectionItemViewWithIcon(showVersion) { AppVersionText() } } @Composable fun AppVersionText() { - Text("v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + Text(appVersionInfo.first + (if (appVersionInfo.second != null) " (" + appVersionInfo.second + ")" else "")) } @Composable fun ProfilePreview(profileOf: NamedChat, size: Dp = 60.dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, textColor: Color = MaterialTheme.colors.onBackground, stopped: Boolean = false) { @@ -411,10 +392,17 @@ fun SettingsActionItemWithContent(icon: Painter?, text: String? = null, click: ( TextIconSpaced(extraPadding) } if (text != null) { - Text(text, Modifier.weight(1f), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground) + val padding = with(LocalDensity.current) { 6.sp.toDp() } + Text(text, Modifier.weight(1f).padding(vertical = padding), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING)) + Row(Modifier.widthIn(max = (windowWidth() - DEFAULT_PADDING * 2) / 2)) { + content() + } + } else { + Row { + content() + } } - content() } } @@ -432,21 +420,6 @@ fun SettingsPreferenceItem( } } -@Composable -fun SettingsPreferenceItemWithInfo( - icon: Painter, - iconTint: Color, - text: String, - stopped: Boolean, - onClickInfo: () -> Unit, - pref: SharedPreference<Boolean>, - prefState: MutableState<Boolean>? = null -) { - SettingsActionItemWithContent(icon, null, click = if (stopped) null else onClickInfo, iconColor = iconTint, extraPadding = true,) { - SharedPreferenceToggleWithIcon(text, painterResource(MR.images.ic_info), stopped, onClickInfo, pref, prefState) - } -} - @Composable fun PreferenceToggle( text: String, @@ -490,32 +463,11 @@ private fun runAuth(title: String, desc: String, onFinish: (success: Boolean) -> ) } -private fun restartApp() { - ProcessPhoenix.triggerRebirth(SimplexApp.context) - shutdownApp() -} - -private fun shutdownApp() { - WorkManager.getInstance(SimplexApp.context).cancelAllWork() - SimplexService.safeStopService(SimplexApp.context) - Runtime.getRuntime().exit(0) -} - -private fun shutdownAppAlert(onConfirm: () -> Unit) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.shutdown_alert_question), - text = generalGetString(MR.strings.shutdown_alert_desc), - destructive = true, - onConfirm = onConfirm - ) -} - -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewSettingsLayout() { SimpleXTheme { @@ -523,8 +475,8 @@ fun PreviewSettingsLayout() { profile = LocalProfile.sampleData, stopped = false, encrypted = false, - incognito = remember { mutableStateOf(false) }, - incognitoPref = SharedPreference({ false }, {}), + passphraseSaved = false, + notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) }, userDisplayName = "Alice", setPerformLA = { _ -> }, showModal = { {} }, @@ -533,6 +485,7 @@ fun PreviewSettingsLayout() { showCustomModal = { {} }, showVersion = {}, withAuth = { _, _, _ -> }, + drawerState = DrawerState(DrawerValue.Closed), ) } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt similarity index 77% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserAddressLearnMore.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index de08ffda66..276c595435 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -1,14 +1,13 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.R -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.ReadableText -import chat.simplex.app.views.onboarding.ReadableTextWithLink +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.views.onboarding.ReadableTextWithLink import chat.simplex.res.MR @Composable diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt similarity index 94% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 2cd8484296..63f06a2aec 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -1,12 +1,11 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionItemView import SectionTextFooter import SectionView -import android.content.res.Configuration -import android.util.Log +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,17 +15,19 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalUriHandler import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.TAG -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.ShareAddressButton -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.newchat.QRCode +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.ShareAddressButton +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.MsgContent +import chat.simplex.common.platform.* import chat.simplex.res.MR @Composable @@ -56,6 +57,8 @@ fun UserAddressView( } } val userAddress = remember { chatModel.userAddress } + val clipboard = LocalClipboardManager.current + val uriHandler = LocalUriHandler.current val showLayout = @Composable { UserAddressLayout( userAddress = userAddress.value, @@ -82,7 +85,7 @@ fun UserAddressView( } }, learnMore = { - ModalManager.shared.showModal { + ModalManager.start.showModal { Column( Modifier .fillMaxHeight() @@ -93,9 +96,9 @@ fun UserAddressView( } } }, - share = { userAddress: String -> shareText(userAddress) }, + share = { userAddress: String -> clipboard.shareText(userAddress) }, sendEmail = { userAddress -> - sendEmail( + uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), generalGetString(MR.strings.email_invite_body).format(userAddress.connReqContact) ) @@ -416,12 +419,11 @@ private fun SaveAASButton(disabled: Boolean, onClick: () -> Unit) { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewUserAddressLayoutNoAddress() { SimpleXTheme { @@ -450,12 +452,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewUserAddressLayoutAddressCreated() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt similarity index 89% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt index ee03a689ec..a7ae3a6751 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfileView.kt @@ -1,8 +1,7 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer -import android.content.res.Configuration -import android.net.Uri +import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape @@ -16,20 +15,19 @@ import androidx.compose.ui.graphics.Color import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.app.R -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.ProfileNameField -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.isValidDisplayName -import chat.simplex.app.views.onboarding.ReadableText -import com.google.accompanist.insets.ProvideWindowInsets -import com.google.accompanist.insets.navigationBarsWithImePadding +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.ProfileNameField +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.isValidDisplayName +import chat.simplex.common.views.onboarding.ReadableText +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.Profile +import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.launch +import java.net.URI @Composable fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { @@ -41,8 +39,9 @@ fun UserProfileView(chatModel: ChatModel, close: () -> Unit) { close, saveProfile = { displayName, fullName, image -> withApi { - val newProfile = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image)) - if (newProfile != null) { + val updated = chatModel.controller.apiUpdateProfile(profile.copy(displayName = displayName, fullName = fullName, image = image)) + if (updated != null) { + val (newProfile, _) = updated chatModel.updateCurrentUser(newProfile) profile = newProfile } @@ -62,7 +61,7 @@ fun UserProfileLayout( val bottomSheetModalState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) val displayName = remember { mutableStateOf(profile.displayName) } val fullName = remember { mutableStateOf(profile.fullName) } - val chosenImage = rememberSaveable { mutableStateOf<Uri?>(null) } + val chosenImage = rememberSaveable { mutableStateOf<URI?>(null) } val profileImage = rememberSaveable { mutableStateOf(profile.image) } val scope = rememberCoroutineScope() val scrollState = rememberScrollState() @@ -217,12 +216,11 @@ private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { ) } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewUserProfileLayoutEditOff() { SimpleXTheme { @@ -234,12 +232,11 @@ fun PreviewUserProfileLayoutEditOff() { } } -@Preview(showBackground = true) -@Preview( +@Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" -) +)*/ @Composable fun PreviewUserProfileLayoutEditOn() { SimpleXTheme { diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt similarity index 92% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index 49024f5977..7929413c93 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -1,4 +1,4 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDivider @@ -20,16 +20,17 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import chat.simplex.app.R -import chat.simplex.app.chatPasswordHash -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* -import chat.simplex.app.views.chat.item.ItemAction -import chat.simplex.app.views.chatlist.UserProfilePickerItem -import chat.simplex.app.views.chatlist.UserProfileRow -import chat.simplex.app.views.database.PassphraseField -import chat.simplex.app.views.helpers.* -import chat.simplex.app.views.onboarding.CreateProfile +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatPasswordHash +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.UserProfilePickerItem +import chat.simplex.common.views.chatlist.UserProfileRow +import chat.simplex.common.views.database.PassphraseField +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.CreateProfile +import chat.simplex.common.model.* +import chat.simplex.common.platform.appPlatform import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay @@ -47,11 +48,15 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: showHiddenProfilesNotice = m.controller.appPrefs.showHiddenProfilesNotice, visibleUsersCount = visibleUsersCount(m), addUser = { - ModalManager.shared.showModalCloseable { close -> + ModalManager.center.showModalCloseable { close -> CreateProfile(m, close) } }, activateUser = { user -> + if (appPlatform.isDesktop) { + ModalManager.center.closeModals() + ModalManager.end.closeModals() + } withBGApi { m.controller.changeActiveUser(user.userId, userViewPassword(user, searchTextOrPassword.value.trim())) } @@ -99,7 +104,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: }, unhideUser = { user -> if (passwordEntryRequired(user, searchTextOrPassword.value)) { - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.start.showModalCloseable(true) { close -> ProfileActionView(UserProfileAction.UNHIDE, user) { pwd -> withBGApi { setUserPrivacy(m) { m.controller.apiUnhideUser(user.userId, pwd) } @@ -122,7 +127,7 @@ fun UserProfilesView(m: ChatModel, search: MutableState<String>, profileHidden: withBGApi { setUserPrivacy(m) { m.controller.apiUnmuteUser(user.userId) } } }, showHiddenProfile = { user -> - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.start.showModalCloseable(true) { close -> HiddenProfileView(m, user) { profileHidden.value = true withBGApi { @@ -159,10 +164,9 @@ private fun UserProfilesLayout( ) { if (profileHidden.value) { SectionView { - SettingsActionItem(painterResource(MR.images.ic_lock_open), stringResource(MR.strings.enter_password_to_show), click = { + SettingsActionItem(painterResource(MR.images.ic_lock_open_right), stringResource(MR.strings.enter_password_to_show), click = { profileHidden.value = false - } - ) + }) } SectionSpacer() } @@ -218,7 +222,7 @@ private fun UserView( Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { DefaultDropdownMenu(showMenu) { if (user.hidden) { - ItemAction(stringResource(MR.strings.user_unhide), painterResource(MR.images.ic_lock_open), onClick = { + ItemAction(stringResource(MR.strings.user_unhide), painterResource(MR.images.ic_lock_open_right), onClick = { showMenu.value = false unhideUser(user) }) @@ -328,7 +332,7 @@ private fun passwordEntryRequired(user: User, searchTextOrPassword: String): Boo private fun removeUser(m: ChatModel, user: User, users: List<User>, delSMPQueues: Boolean, searchTextOrPassword: String) { if (passwordEntryRequired(user, searchTextOrPassword)) { - ModalManager.shared.showModalCloseable(true) { close -> + ModalManager.start.showModalCloseable(true) { close -> ProfileActionView(UserProfileAction.DELETE, user) { pwd -> withBGApi { doRemoveUser(m, user, users, delSMPQueues, pwd) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt similarity index 55% rename from apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/VersionInfoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index e7eab47993..8528414771 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -1,17 +1,16 @@ -package chat.simplex.app.views.usersettings +package chat.simplex.common.views.usersettings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource -import chat.simplex.app.BuildConfig -import chat.simplex.app.R -import chat.simplex.app.model.CoreVersionInfo -import chat.simplex.app.ui.theme.DEFAULT_PADDING -import chat.simplex.app.views.helpers.AppBarTitle +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.CoreVersionInfo +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.views.helpers.AppBarTitle import chat.simplex.res.MR @Composable @@ -20,8 +19,12 @@ fun VersionInfoView(info: CoreVersionInfo) { Modifier.padding(horizontal = DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.app_version_title), false) - Text(String.format(stringResource(MR.strings.app_version_name), BuildConfig.VERSION_NAME)) - Text(String.format(stringResource(MR.strings.app_version_code), BuildConfig.VERSION_CODE)) + if (appPlatform.isAndroid) { + Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) + Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE)) + } else { + Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME)) + } Text(String.format(stringResource(MR.strings.core_version), info.version)) val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit)) diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 21a1c45bf0..65767087be 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -1,7 +1,7 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string name="accept_contact_button">اقبل</string> - <string name="about_simplex_chat">عن ٍSimpleX </string> + <string name="about_simplex_chat">عن SimpleX Chat</string> <string name="a_plus_b">a + b</string> <string name="accept">اقبل</string> <string name="chat_item_ttl_week">اسبوع 1</string> @@ -10,7 +10,7 @@ <string name="chat_item_ttl_day">يوم 1</string> <string name="accept_feature">اقبل</string> <string name="about_simplex">عن SimpleX</string> - <string name="above_then_preposition_continuation">أعلاه ، ثم:</string> + <string name="above_then_preposition_continuation">أعلاه، ثم:</string> <string name="accept_call_on_lock_screen">اقبل</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string> <string name="alert_message_no_group">هذه المجموعة لم تعد موجودة.</string> @@ -20,11 +20,11 @@ <string name="enable_automatic_deletion_message">لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق.</string> <string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string> - <string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات الاتصال الخاصة بك فقط.</string> + <string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string> <string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string> <string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيتلقى العضو دعوة جديدة.</string> <string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string> - <string name="switch_receiving_address_desc">هذه الميزة تجريبية! ستعمل فقط إذا كان لدى العميل الآخر الإصدار 4.2 مثبتًا. يجب أن ترى الرسالة في المحادثة بمجرد اكتمال تغيير العنوان - يرجى التحقق من أنه لا يزال بإمكانك تلقي الرسائل من جهة الاتصال هذه (أو عضو المجموعة).</string> + <string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string> <string name="this_link_is_not_a_valid_connection_link">هذا الارتباط ليس ارتباط اتصال صالح!</string> <string name="allow_verb">يسمح</string> <string name="smp_servers_preset_add">أضف خوادم محددة مسبقًا</string> @@ -34,7 +34,7 @@ <string name="smp_servers_add">إضافة خادم …</string> <string name="network_settings">إعدادات الشبكة المتقدمة</string> <string name="all_group_members_will_remain_connected">سيبقى جميع أعضاء المجموعة على اتصال.</string> - <string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة الاتصال الخاصة بك بذلك.</string> + <string name="allow_disappearing_messages_only_if">السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="allow_irreversible_message_deletion_only_if">السماح بحذف الرسائل بشكل لا رجوع فيه فقط إذا سمحت لك جهة الاتصال بذلك.</string> <string name="group_member_role_admin">مسؤل</string> <string name="users_add">إضافة ملف التعريف</string> @@ -46,8 +46,8 @@ <string name="accept_connection_request__question">قبول طلب الاتصال؟</string> <string name="clear_chat_warning">سيتم حذف جميع الرسائل - لا يمكن التراجع عن هذا! سيتم حذف الرسائل فقط من أجلك.</string> <string name="callstatus_accepted">مكالمة مقبولة</string> - <string name="allow_calls_only_if">السماح بالمكالمات فقط إذا سمحت جهة الاتصال الخاصة بك بذلك.</string> - <string name="allow_message_reactions_only_if">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة الاتصال الخاصة بك بذلك.</string> + <string name="allow_calls_only_if">السماح بالمكالمات فقط إذا سمحت جهة اتصالك بذلك.</string> + <string name="allow_message_reactions_only_if">اسمح بردود الفعل على الرسائل فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="keychain_is_storing_securely">يتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن - فهو يسمح لخدمة الإشعارات بالعمل.</string> <string name="empty_chat_profile_is_created">يتم إنشاء ملف تعريف دردشة فارغ بالاسم المقدم ، ويفتح التطبيق كالمعتاد.</string> <string name="answer_call">أجب الاتصال</string> @@ -56,13 +56,13 @@ <string name="allow_to_send_voice">السماح بإرسال رسائل صوتية.</string> <string name="settings_section_title_app">تطبيق</string> <string name="color_secondary_variant">ثانوي إضافي</string> - <string name="allow_your_contacts_adding_message_reactions">السماح لجهات الاتصال الخاصة بك بإضافة ردود الفعل الرسالة.</string> - <string name="allow_your_contacts_to_call">اسمح لجهات الاتصال الخاصة بك بالاتصال بك.</string> + <string name="allow_your_contacts_adding_message_reactions">السماح لجهات اتصالك بإضافة ردود الفعل الرسالة.</string> + <string name="allow_your_contacts_to_call">السماح لجهات اتصالك بالاتصال بك.</string> <string name="allow_message_reactions">السماح بردود الفعل على الرسائل.</string> <string name="v5_1_self_destruct_passcode_descr">يتم مسح جميع البيانات عند إدخالها.</string> <string name="keychain_allows_to_receive_ntfs">سيتم استخدام Android Keystore لتخزين عبارة المرور بشكل آمن بعد إعادة تشغيل التطبيق أو تغيير عبارة المرور - سيسمح بتلقي الإشعارات.</string> - <string name="allow_your_contacts_to_send_disappearing_messages">السماح لجهات الاتصال الخاصة بك بإرسال رسائل تختفي.</string> - <string name="allow_voice_messages_only_if">اسمح بالرسائل الصوتية فقط إذا سمحت جهة الاتصال الخاصة بك بذلك.</string> + <string name="allow_your_contacts_to_send_disappearing_messages">السماح لجهات اتصالك بإرسال رسائل تختفي.</string> + <string name="allow_voice_messages_only_if">اسمح بالرسائل الصوتية فقط إذا سمحت جهة اتصالك بذلك.</string> <string name="v5_0_app_passcode">رمز مرور التطبيق</string> <string name="notifications_mode_service">دائِماً مُتاح</string> <string name="notifications_mode_off_desc">يمكن للتطبيق تلقي الإشعارات فقط عند تشغيله ، ولن يتم بدء تشغيل أي خدمة في الخلفية</string> @@ -72,7 +72,7 @@ <string name="full_backup">النسخ الاحتياطي لبيانات التطبيق</string> <string name="all_app_data_will_be_cleared">حُذفت جميع بيانات التطبيق.</string> <string name="allow_to_delete_messages">السماح بحذف الرسائل المرسلة بشكل لا رجعة فيه.</string> - <string name="allow_your_contacts_to_send_voice_messages">اسمح لجهات الاتصال الخاصة بك بإرسال رسائل صوتية.</string> + <string name="allow_your_contacts_to_send_voice_messages">اسمح لجهات اتصالك بإرسال رسائل صوتية.</string> <string name="learn_more_about_address">حول عنوان SimpleX</string> <string name="app_version_code">بناء التطبيق: %s</string> <string name="appearance_settings">المظهر</string> @@ -80,7 +80,7 @@ <string name="all_your_contacts_will_remain_connected_update_sent">ستبقى جميع جهات الاتصال الخاصة بك متصلة. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string> <string name="settings_section_title_icon">رمز التطبيق</string> <string name="address_section_title">عنوان</string> - <string name="allow_your_contacts_irreversibly_delete">اسمح لجهات الاتصال الخاصة بك بحذف الرسائل المرسلة بشكل لا رجعة فيه.</string> + <string name="allow_your_contacts_irreversibly_delete">اسمح لجهات اتصالك بحذف الرسائل المرسلة بشكل لا رجعة فيه.</string> <string name="auth_unavailable">المصادقة غير متاحة</string> <string name="back">رجوع</string> <string name="invite_prohibited">لا يمكن دعوة جهة اتصال!</string> @@ -93,8 +93,8 @@ <string name="send_disappearing_message_5_minutes">5 دقائق</string> <string name="authentication_cancelled">ألغيت المصادقة</string> <string name="notifications_mode_service_desc">تعمل خدمة الخلفية دائمًا - سيتم عرض الإشعارات بمجرد توفر الرسائل.</string> - <string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة الاتصال الخاصة بك إضافة ردود فعل الرسائل.</string> - <string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة الاتصال الخاصة بك إرسال رسائل تختفي.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">يمكنك أنت وجهة اتصالك إضافة ردود فعل الرسائل.</string> + <string name="both_you_and_your_contact_can_send_disappearing">يمكنك أنت وجهة اتصالك إرسال رسائل تختفي.</string> <string name="icon_descr_call_progress">مكالمتك تحت الإجراء</string> <string name="cannot_receive_file">لا يمكن استقبال الملف</string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> جيد للبطارية </b>. خدمة الخلفية تتحقق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]></string> @@ -114,8 +114,8 @@ <string name="v4_5_transport_isolation_descr">عن طريق ملف تعريف الدردشة (افتراضي) أو عن طريق الاتصال (تجريبي).</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> يمكن تعطيله عبر الإعدادات</b> - سيستمر عرض الإشعارات أثناء تشغيل التطبيق.]]></string> <string name="settings_audio_video_calls">مكالمات الصوت والفيديو</string> - <string name="impossible_to_recover_passphrase"><![CDATA[<b> الرجاء ملاحظة </b>: لن تتمكن من استعادة عبارة المرور أو تغييرها في حالة فقدها.]]></string> - <string name="both_you_and_your_contacts_can_delete">يمكنك أنت وجهة الاتصال الخاصة بك حذف الرسائل المرسلة بشكل لا رجعة فيه.</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>يُرجى الملاحظة</b>: لن تتمكن من استعادة عبارة المرور أو تغييرها في حالة فقدها.]]></string> + <string name="both_you_and_your_contacts_can_delete">يمكنك أنت وجهة اتصالك حذف الرسائل المرسلة بشكل لا رجعة فيه.</string> <string name="v4_2_auto_accept_contact_requests">قبول طلبات الاتصال تلقائيًا</string> <string name="la_auth_failed">فشلت المصادقة</string> <string name="la_authenticate">مصادقة</string> @@ -147,7 +147,7 @@ <string name="rcv_conn_event_switch_queue_phase_completed">تم تغيير العنوان من أجلك</string> <string name="cant_delete_user_profile">لا يمكن حذف ملف تعريف المستخدم!</string> <string name="icon_descr_video_asked_to_receive">طلب لاستلام الفيديو</string> - <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة الاتصال الخاصة بك.]]></string> + <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> إضافة جهة اتصال جديدة </b>: لإنشاء رمز الاستجابة السريعة الخاص بك لمرة واحدة لجهة اتصالك.]]></string> <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b> امسح رمز الاستجابة السريعة </b>: للاتصال بجهة الاتصال التي تعرض لك رمز الاستجابة السريعة.]]></string> <string name="callstatus_in_progress">مكالمتك تحت الإجراء</string> <string name="callstatus_ended">انتهت المكالمة</string> @@ -155,12 +155,10 @@ <string name="cannot_access_keychain">لا يمكن الوصول إلى Keystore لحفظ كلمة مرور قاعدة البيانات</string> <string name="icon_descr_cancel_file_preview">إلغاء معاينة الملف</string> <string name="app_version_title">نسخة التطبيق</string> - <string name="incognito_random_profile_from_contact_description">سيتم إرسال ملف تعريف عشوائي إلى جهة الاتصال التي تلقيت منها هذا الارتباط</string> - <string name="incognito_random_profile_description">سيتم إرسال ملف تعريف عشوائي إلى جهة الاتصال الخاصة بك</string> <string name="color_background">الخلفية</string> <string name="audio_video_calls">مكالمات الصوت/الفيديو</string> <string name="v5_1_better_messages">رسائل أفضل</string> - <string name="both_you_and_your_contact_can_send_voice">يمكنك أنت وجهة الاتصال الخاصة بك إرسال رسائل صوتية.</string> + <string name="both_you_and_your_contact_can_send_voice">يمكنك أنت وجهة اتصالك إرسال رسائل صوتية.</string> <string name="both_you_and_your_contact_can_make_calls">يمكنك أنت وجهة الاتصال إجراء مكالمات.</string> <string name="search_verb">بحث</string> <string name="la_mode_off">غير مفعّل</string> @@ -208,7 +206,7 @@ <string name="create_simplex_address">إنشاء عنوان SimpleX</string> <string name="continue_to_next_step">متابعة</string> <string name="chat_with_developers">تحدث مع المطورين</string> - <string name="icon_descr_context">رمز السياق</string> + <string name="icon_descr_context">سياق الأيقونة</string> <string name="abort_switch_receiving_address_question">إحباط تغيير العنوان؟</string> <string name="abort_switch_receiving_address_confirm">إحباط</string> <string name="abort_switch_receiving_address_desc">سيتم إحباط تغيير العنوان. سيتم استخدام عنوان الاستلام القديم.</string> @@ -233,7 +231,7 @@ <string name="info_row_connection">الاتصال</string> <string name="abort_switch_receiving_address">إحباط تغيير العنوان</string> <string name="create_secret_group_title">إنشاء مجموعة سرية</string> - <string name="v4_4_verify_connection_security_desc">قارن رموز الأمان مع جهات الاتصال الخاصة بك.</string> + <string name="v4_4_verify_connection_security_desc">قارن رموز الأمان مع جهات اتصالك.</string> <string name="v4_6_chinese_spanish_interface">الواجهة الصينية والاسبانية</string> <string name="clear_chat_menu_action">مسح</string> <string name="contact_wants_to_connect_via_call">%1$s يريد التواصل معك عبر</string> @@ -414,7 +412,7 @@ <string name="settings_developer_tools">أدوات المطور</string> <string name="smp_server_test_delete_queue">حذف قائمة الانتظار</string> <string name="error_updating_user_privacy">خطأ في تحديث خصوصية المستخدم</string> - <string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 سطح المكتب: امسح رمز الاستجابة السريعة (QR) المعروض من التطبيق، عبر <b>مسح رمز QR</b>.]]></string> <string name="delete_profile">حذف ملف التعريف</string> <string name="smp_servers_delete_server">حذف الخادم</string> <string name="error_updating_link_for_group">خطأ في تحديث ارتباط المجموعة</string> @@ -463,7 +461,7 @@ <string name="full_name__field">الاسم الكامل:</string> <string name="alert_message_group_invitation_expired">لم تعد دعوة المجموعة صالحة، تمت أُزيلت بواسطة المرسل.</string> <string name="group_link">رابط المجموعة</string> - <string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة الاتصال الخاصة بك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="file_will_be_received_when_contact_is_online">سيتم استلام الملف عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> <string name="group_full_name_field">الاسم الكامل للمجموعة:</string> <string name="simplex_link_mode_full">رابط كامل</string> <string name="choose_file">الملف</string> @@ -497,8 +495,8 @@ <string name="import_database">استيراد قاعدة بيانات</string> <string name="custom_time_unit_hours">ساعات</string> <string name="edit_history">السجل</string> - <string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عند اكتمال تحميل جهة الاتصال الخاصة بك.</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز الاستجابة السريعة في مكالمة الفيديو</b>، أو شارك الرابط.</string> + <string name="image_will_be_received_when_contact_completes_uploading">سيتم استلام الصورة عند اكتمال تحميل جهة اتصالك.</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، <b>اعرض رمز الاستجابة السريعة في مكالمة الفيديو</b>، أو شارك الرابط.]]></string> <string name="install_simplex_chat_for_terminal">ثبّت SimpleX Chat لطرفية</string> <string name="network_disable_socks_info">إذا قمت بالتأكيد، فستتمكن خوادم المراسلة من رؤية عنوان IP الخاص بك ومزود الخدمة الخاص بك - أي الخوادم التي تتصل بها.</string> <string name="hide_dev_options">إخفاء:</string> @@ -510,23 +508,22 @@ <string name="v4_3_improved_server_configuration">تحسن تضبيط الخادم</string> <string name="ignore">تجاهل</string> <string name="incorrect_passcode">رمز المرور غير صحيح</string> - <string name="group_unsupported_incognito_main_profile_sent">وضع التخفي غير مدعوم هنا - سيتم إرسال ملف التعريف الرئيسي الخاص بك إلى أعضاء المجموعة</string> <string name="user_hide">إخفاء</string> <string name="incoming_audio_call">مكالمة صوتية واردة</string> <string name="conn_level_desc_indirect">غير مباشر (%1$s)</string> <string name="how_it_works">آلية العمل</string> <string name="incoming_video_call">مكالمة فيديو واردة</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">إذا تلقيت رابط دعوة SimpleX Chat، فيمكنك فتحه في متصفحك:</string> - <string name="incognito_info_protects">يحمي وضع التخفي خصوصية اسم وصورة ملف التعريف الرئيسي - يتم إنشاء ملف تعريف عشوائي جديد لكل جهة اتصال جديدة.</string> + <string name="incognito_info_protects">يحمي وضع التخفي خصوصيتك بأستخدام ملف تعريف عشوائي جديد لكل جهة اتصال جديدة.</string> <string name="info_menu">المعلومات</string> <string name="v4_3_improved_privacy_and_security_desc">إخفاء شاشة التطبيق في التطبيقات الحديثة.</string> <string name="v4_3_improved_privacy_and_security">تحسن الخصوصية والأمان</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">إذا لم تتمكن من الالتقاء شخصيًا، فيمكنك <b>مسح رمز QR في مكالمة الفيديو</b>، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[إذا لم تتمكن من الالتقاء شخصيًا، فيمكنك <b>مسح رمز QR في مكالمة الفيديو</b>، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.]]></string> <string name="immune_to_spam_and_abuse">محصن ضد البريد العشوائي وسوء المعاملة</string> <string name="description_via_one_time_link_incognito">التخفي عبر رابط لمرة واحدة</string> <string name="icon_descr_image_snd_complete">أرسلت صورة</string> <string name="image_descr">صورة</string> - <string name="image_will_be_received_when_contact_is_online">سيتم استلام الصورة عندما تكون جهة الاتصال الخاصة بك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="image_will_be_received_when_contact_is_online">سيتم استلام الصورة عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> <string name="image_saved">حُفظت الصورة في المعرض</string> <string name="gallery_image_button">صورة</string> <string name="if_you_cant_meet_in_person">إذا لم تتمكن من الالتقاء شخصيًا، اعرض رمز الاستجابة السريعة في مكالمة الفيديو، أو شارك الرابط.</string> @@ -541,7 +538,7 @@ <string name="onboarding_notifications_mode_service">فوري</string> <string name="host_verb">المضيف</string> <string name="hide_notification">إخفاء</string> - <string name="turn_off_battery_optimization">من أجل استخدامها، يرجى <b>تعطيل تحسين البطارية</b> لSimpleX في مربع الحوار التالي. وإلا، سيتم تعطيل الإخطارات.</string> + <string name="turn_off_battery_optimization"><![CDATA[من أجل استخدامها، <b>يرجى السماح لSimpleX للتشغيل في الخلفية</b> في مربع الحوار التالي. وإلا، سيتم تعطيل الإخطارات.]]></string> <string name="in_reply_to">ردًا على</string> <string name="icon_descr_instant_notifications">إشعارات فورية</string> <string name="enter_one_ICE_server_per_line">خوادم ICE (واحد لكل سطر)</string> @@ -558,7 +555,7 @@ <string name="icon_descr_add_members">دعوة الأعضاء</string> <string name="alert_text_skipped_messages_it_can_happen_when">يمكن أن يحدث عندما: \n1. انتهت صلاحية الرسائل في العميل المرسل بعد يومين أو على الخادم بعد 30 يومًا. -\n2. فشل فك تشفير الرسالة، لأنك أو جهة الاتصال الخاصة بك استخدمت نسخة احتياطية قديمة من قاعدة البيانات. +\n2. فشل فك تشفير الرسالة، لأنك أو جهة اتصالك استخدمت نسخة احتياطية قديمة من قاعدة البيانات. \n3. اُخترق الاتصال.</string> <string name="v5_1_japanese_portuguese_interface">واجهة أستخدام يابانية وبرتغالية</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">يمكن أن يحدث ذلك عندما تستخدم أنت أو اتصالك النسخة الاحتياطية القديمة لقاعدة البيانات.</string> @@ -587,7 +584,7 @@ <string name="large_file">الملف كبير!</string> <string name="learn_more">معرفة المزيد</string> <string name="v4_3_irreversible_message_deletion">حذف رسالة لا رجعة فيه</string> - <string name="v4_4_live_messages">رسائل مباشرة</string> + <string name="v4_4_live_messages">رسائل حيّة</string> <string name="smp_servers_invalid_address">عنوان الخادم غير صالح!</string> <string name="invalid_migration_confirmation">تأكيد الترحيل غير صالح</string> <string name="group_member_status_invited">مدعو</string> @@ -716,7 +713,7 @@ <string name="ensure_ICE_server_address_are_correct_format_and_unique">تأكد من أن عناوين خادم WebRTC ICE بالتنسيق الصحيح، وأن تكون مفصولة بأسطر وليست مكررة.</string> <string name="mark_code_verified">وضع علامة تَحقق منه</string> <string name="error_saving_user_password">خطأ في حفظ كلمة مرور المستخدم</string> - <string name="many_people_asked_how_can_it_deliver">سأل الكثير من الناس: <i>إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟</i></string> + <string name="many_people_asked_how_can_it_deliver"><![CDATA[سأل الكثير من الناس: <i>إذا SimpleX ليس لديه معرّفات مستخدم، كيف يمكنه توصيل الرسائل؟</i>]]></string> <string name="error_saving_group_profile">خطأ في حفظ ملف تعريف المجموعة</string> <string name="notification_preview_mode_message">رسالة نصية</string> <string name="message_reactions">ردود فعل الرسائل</string> @@ -725,7 +722,7 @@ <string name="message_delivery_error_title">خطأ في تسليم الرسالة</string> <string name="network_and_servers">الشبكة والخوادم</string> <string name="moderate_verb">إشراف</string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app">📱 للجوال: انقر فوق <b>فتح في تطبيق الجوال</b> ، ثم انقر فوق <b>اتصال</b> في التطبيق.</string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 للجوال: انقر فوق <b>فتح في تطبيق الجوال</b> ، ثم انقر فوق <b>اتصال</b> في التطبيق.]]></string> <string name="share_text_moderated_at">تحت الإشراف في: %s</string> <string name="message_reactions_prohibited_in_this_chat">ردود الفعل الرسائل ممنوعة في هذه الدردشة.</string> <string name="moderated_item_description">مُشرف بواسطة %s</string> @@ -752,7 +749,7 @@ <string name="user_mute">كتم</string> <string name="message_reactions_are_prohibited">ردود الفعل الرسائل ممنوعة في هذه المجموعة.</string> <string name="icon_descr_more_button">المزيد</string> - <string name="network_settings_title">اعدادات الشبكة</string> + <string name="network_settings_title">إعدادات الشبكة</string> <string name="icon_descr_call_missed">مكالمة فائتة</string> <string name="v4_5_message_draft">مسودة الرسالة</string> <string name="v4_5_multiple_chat_profiles">ملفات تعريف دردشة متعددة</string> @@ -797,9 +794,609 @@ <string name="videos_limit_desc">يمكن إرسال 10 فيديوهات فقط في نفس الوقت</string> <string name="add_contact">رابط دعوة لمرة واحدة</string> <string name="network_use_onion_hosts_no">لا</string> - <string name="network_use_onion_hosts_required_desc">سوف تكون مضيفات البصل مطلوبة للاتصال.</string> + <string name="network_use_onion_hosts_required_desc">سوف تكون مضيفات البصل مطلوبة للاتصال. +\nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">سيتم استخدام مضيفات البصل عند توفرها.</string> <string name="network_use_onion_hosts_no_desc_in_alert">لن يتم استخدام مضيفات البصل.</string> <string name="self_destruct_new_display_name">اسم عرض جديد:</string> <string name="new_passphrase">عبارة مرور جديدة…</string> + <string name="icon_descr_server_status_pending">يرجى الانتظار</string> + <string name="enter_passphrase_notification_title">كلمة المرور مطلوبة</string> + <string name="paste_the_link_you_received">ألصق الرابط المستلم</string> + <string name="only_owners_can_enable_files_and_media">فقط أصحاب المجموعة يمكنهم تفعيل الملفات والوسائط.</string> + <string name="only_group_owners_can_enable_voice">فقط أصحاب المجموعة يمكنهم تفعيل الرسائل الصوتية.</string> + <string name="only_stored_on_members_devices">(يخزن فقط بواسطة أعضاء المجموعة)</string> + <string name="la_mode_passcode">كلمة المرور</string> + <string name="passcode_set">تم تعيين كلمة المرور!</string> + <string name="group_member_role_owner">صاحب</string> + <string name="only_your_contact_can_send_disappearing">فقط جهة اتصالك يمكنها إرسال رسائل مؤقتة.</string> + <string name="only_your_contact_can_add_message_reactions">جهة اتصالك فقط يمكنها إضافة تفاعلات على الرسالة</string> + <string name="only_group_owners_can_change_prefs">فقط أصحاب المجموعة يمكنهم تغيير إعداداتها.</string> + <string name="only_your_contact_can_delete">جهة اتصالك فقط يمكنها حذف الرسائل نهائيا (يمكنك تعليم الرسالة للحذف).</string> + <string name="only_you_can_send_voice">أنت فقط يمكنك إرسال رسائل صوتية.</string> + <string name="open_verb">افتح</string> + <string name="passcode_not_changed">لم يتم تغيير كلمة المرور!</string> + <string name="passcode_changed">تم تغيير كلمة المرور</string> + <string name="opening_database">يتم فتح قاعدة البيانات…</string> + <string name="only_your_contact_can_send_voice">جهة اتصالك فقط يمكنها إرسال رسائل صوتية.</string> + <string name="paste_button">ألصق</string> + <string name="restore_passphrase_not_found_desc">كلمة المرور غير موجودة في مخزن المفاتيح، يرجى إدخالها يدوياً. قد يحدث هذا إذا قمت باستعادة ملفات التطبيق باستخدام أداة استرجاع بيانات. إذا لم يكن الأمر كذلك، تواصل مع المبرمجين رجاء</string> + <string name="open_chat">افتح الدردشة</string> + <string name="simplex_link_mode_browser_warning">فتح الرابط في المتصفح قد يقلل خصوصية وحماية اتصالك. الروابط غير الموثوقة من SimpleX ستكون باللون الأحمر</string> + <string name="only_you_can_add_message_reactions">أنت فقط يمكنك إضافة تفاعل على الرسالة.</string> + <string name="only_you_can_delete_messages">أنت فقط يمكنك حذف الرسائل نهائيا (يمكن للمستلم تعليمها للحذف)</string> + <string name="only_you_can_send_disappearing">أنت فقط يمكنك إرسال رسائل مؤقتة</string> + <string name="only_you_can_make_calls">أنت فقط يمكنك إجراء المكالمات.</string> + <string name="only_your_contact_can_make_calls">فقط جهة اتصالك يمكنها إجراء المكالمات.</string> + <string name="auth_open_chat_console">افتح وحدة تحكم الدردشة</string> + <string name="la_lock_mode_passcode">إدخال كلمة المرور</string> + <string name="open_simplex_chat_to_accept_call">"قم بفتح SimpleX Chat للرد على المكالمة"</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">بروتوكول وكود مفتوح المصدر - يمكن لأي شخص تشغيل الخوادم.</string> + <string name="password_to_show">كلمة المرور للإظهار</string> + <string name="call_connection_peer_to_peer">ند لند</string> + <string name="people_can_connect_only_via_links_you_share">يمكن للناس التواصل معك فقط عبر الرابط الذي تقوم بمشاركته</string> + <string name="icon_descr_call_pending_sent">مكالمة في الانتظار</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[تقوم أجهزة العميل فقط بتخزين ملفات تعريف المستخدمين وجهات الاتصال والمجموعات والرسائل المرسلة باستخدام <b>تشفير ثنائي الطبقات من بين الطريفين</b>.]]></string> + <string name="reset_color">إعادة تعيين الألوان</string> + <string name="save_verb">حفظ</string> + <string name="smp_servers_preset_address">عنوان الخادم المحدد مسبقًا</string> + <string name="save_and_notify_group_members">حفظ وإشعار أعضاء المجموعة</string> + <string name="onboarding_notifications_mode_periodic">دوري</string> + <string name="restart_the_app_to_use_imported_chat_database">أعد تشغيل التطبيق لاستخدام قاعدة بيانات الدردشة المستوردة.</string> + <string name="receiving_via">الاستلام عبر</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">يرجى التحقق من استخدامك للرابط الصحيح أو اطلب من جهة اتصالك أن ترسل لك رابطًا آخر.</string> + <string name="periodic_notifications_disabled">الإشعارات الدورية مُعطَّلة</string> + <string name="image_descr_profile_image">صورة الملف الشخصي</string> + <string name="onboarding_notifications_mode_title">الإشعارات خاصة</string> + <string name="store_passphrase_securely_without_recover">يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها.</string> + <string name="contact_developers">يرجى تحديث التطبيق والاتصال بالمطورين.</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">دليل المستخدم</font>.]]></string> + <string name="auth_open_chat_profiles">افتح ملفات تعريف الدردشة</string> + <string name="revoke_file__confirm">اسحب الوصول</string> + <string name="save_archive">حفظ الأرشيف</string> + <string name="save_color">حفظ اللون</string> + <string name="reveal_verb">كشف</string> + <string name="stop_rcv_file__message">سيتم إيقاف استلام الملف.</string> + <string name="reject_contact_button">رفض</string> + <string name="rate_the_app">قيم التطبيق</string> + <string name="port_verb">منفذ</string> + <string name="save_auto_accept_settings">حفظ إعدادات القبول التلقائي</string> + <string name="privacy_redefined">إعادة تعريف الخصوصية</string> + <string name="alert_text_fragment_please_report_to_developers">الرجاء الإبلاغ للمطورين.</string> + <string name="privacy_and_security">الخصوصية و الأمان</string> + <string name="remove_passphrase">إزالة</string> + <string name="remove_passphrase_from_keychain">إزالة عبارة المرور من Keystore؟</string> + <string name="enter_correct_current_passphrase">الرجاء إدخال عبارة المرور الحالية الصحيحة.</string> + <string name="store_passphrase_securely">يرجى تخزين عبارة المرور بشكل آمن، فلن تتمكن من الوصول إلى الدردشة إذا فقدتها.</string> + <string name="prohibit_calls">منع مكالمات الصوت/الفيديو.</string> + <string name="prohibit_message_deletion">منع حذف الرسائل التي لا رجعة فيها.</string> + <string name="receiving_files_not_yet_supported">استلام الملفات غير معتمد حتى الآن</string> + <string name="network_error_desc">الرجاء التحقق من اتصالك بالشبكة مع %1$s وحاول مرة أخرى.</string> + <string name="saved_ICE_servers_will_be_removed">ستتم إزالة خوادم WebRTC ICE المحفوظة.</string> + <string name="prohibit_sending_files">منع إرسال الملفات والوسائط.</string> + <string name="callstate_received_answer">استلمت إجابة…</string> + <string name="read_more_in_github_with_link"><![CDATA[اقرأ المزيد في <font color="#0088ff">مستودع GitHub</font>.]]></string> + <string name="reject">رفض</string> + <string name="relay_server_protects_ip">يحمي خادم الترحيل عنوان IP الخاص بك، ولكن يمكنه مراقبة مدة المكالمة.</string> + <string name="restore_database_alert_desc">الرجاء إدخال كلمة المرور السابقة بعد استعادة نسخة احتياطية لقاعدة البيانات. لا يمكن التراجع عن هذا الإجراء.</string> + <string name="restore_database_alert_title">استعادة النسخة الاحتياطية لقاعدة البيانات؟</string> + <string name="network_options_save">حفظ</string> + <string name="users_delete_with_connections">اتصالات الملف الشخصي والخادم</string> + <string name="prohibit_message_reactions">منع ردود فعل الرسالة.</string> + <string name="prohibit_sending_voice">منع إرسال الرسائل الصوتية.</string> + <string name="prohibit_message_reactions_group">منع ردود فعل الرسائل.</string> + <string name="whats_new_read_more">قراءة المزيد</string> + <string name="v4_5_reduced_battery_usage">انخفاض استخدام البطارية</string> + <string name="icon_descr_record_voice_message">سجل رسالة صوتية</string> + <string name="network_use_onion_hosts_required">‎مطلوب</string> + <string name="restart_the_app_to_create_a_new_chat_profile">أعد تشغيل التطبيق لإنشاء ملف تعريف دردشة جديد.</string> + <string name="role_in_group">الدور</string> + <string name="callstate_received_confirmation">استلمت التأكيد…</string> + <string name="periodic_notifications">إشعارات دورية</string> + <string name="notifications_mode_off">يعمل عندما يكون التطبيق مفتوحًا</string> + <string name="received_message">استلمت رسالة</string> + <string name="reply_verb">رد</string> + <string name="icon_descr_call_rejected">رُفضت المكالمة</string> + <string name="info_row_updated_at">حٌديثت السجل في</string> + <string name="share_text_updated_at">حٌديثت السجل في: %s</string> + <string name="restore_database_alert_confirm">استعادة</string> + <string name="network_options_revert">إرجاع</string> + <string name="v4_4_live_messages_desc">يرى المستلمون التحديثات أثناء كتابتها.</string> + <string name="feature_received_prohibited">استلمت، ممنوع</string> + <string name="save_servers_button">حفظ</string> + <string name="profile_update_will_be_sent_to_contacts">سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بك.</string> + <string name="save_and_notify_contacts">حفظ وإشعار جهات الاتصال</string> + <string name="save_and_update_group_profile">حفظ وتحديث ملف تعريف المجموعة</string> + <string name="network_option_ping_count">عدد البينج</string> + <string name="color_received_message">استلمت رسالة</string> + <string name="v5_0_polish_interface">الواجهة البولندية</string> + <string name="run_chat_section">تشغيل الدردشة</string> + <string name="restore_database">استعادة النسخة الاحتياطية لقاعدة البيانات</string> + <string name="database_restore_error">خطأ في استعادة قاعدة البيانات</string> + <string name="rcv_group_event_member_deleted">أُزيلت %1$s</string> + <string name="rcv_group_event_user_deleted">أزالتك</string> + <string name="info_row_received_at">استلمت في</string> + <string name="button_remove_member">إزالة العضو</string> + <string name="remove_member_confirmation">إزالة</string> + <string name="network_options_reset_to_defaults">إعادة التعيين إلى الإعدادات الافتراضية</string> + <string name="network_option_ping_interval">بينج الفاصل الزمني</string> + <string name="profile_password">كلمة مرور الملف الشخصي</string> + <string name="prohibit_sending_disappearing_messages">منع إرسال الرسائل التي تختفي.</string> + <string name="network_option_protocol_timeout">مهلة البروتوكول</string> + <string name="network_option_protocol_timeout_per_kb">مهلة البروتوكول لكل كيلوبايت</string> + <string name="prohibit_sending_voice_messages">منع إرسال الرسائل الصوتية.</string> + <string name="prohibit_direct_messages">منع إرسال رسائل مباشرة إلى الأعضاء.</string> + <string name="prohibit_sending_disappearing">منع إرسال الرسائل التي تختفي.</string> + <string name="v4_5_message_draft_descr">الاحتفاظ بمسودة الرسالة الأخيرة، مع المرفقات.</string> + <string name="v4_5_private_filenames">أسماء ملفات خاصة</string> + <string name="v4_6_hidden_chat_profiles_descr">حماية ملفات تعريف الدردشة الخاصة بك بكلمة مرور!</string> + <string name="callstatus_rejected">رُفضت المكالمة</string> + <string name="protect_app_screen">حماية شاشة التطبيق</string> + <string name="group_member_status_removed">أُزيلت</string> + <string name="la_please_remember_to_store_password">يرجى تذكرها أو تخزينها بأمان - لا توجد طريقة لاستعادة كلمة المرور المفقودة!</string> + <string name="group_welcome_preview">معاينة</string> + <string name="error_smp_test_certificate">من المحتمل أن الملف المرجعي للشهادة في عنوان الخادم غير صحيح</string> + <string name="simplex_service_notification_text">يتم استلام الرسائل…</string> + <string name="observer_cant_send_message_desc">يرجى الاتصال بمسؤول المجموعة.</string> + <string name="sync_connection_force_confirm">أعد التفاوض</string> + <string name="sync_connection_force_question">إعادة تفاوض التشفير</string> + <string name="revoke_file__action">سحب وصول الملف</string> + <string name="revoke_file__title">سحب وصول الملف؟</string> + <string name="toast_permission_denied">رٌفض الإذن!</string> + <string name="ask_your_contact_to_enable_voice">يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية.</string> + <string name="icon_descr_profile_image_placeholder">العنصر النائب لصورة الملف الشخصي</string> + <string name="image_descr_qr_code">رمز QR</string> + <string name="reset_verb">إعادة التعيين</string> + <string name="network_proxy_port">المنفذ %d</string> + <string name="smp_servers_preset_server">خادم محدد مسبقًا</string> + <string name="read_more_in_github">قراءة المزيد في مستودعنا على GitHub.</string> + <string name="relay_server_if_necessary">يتم استخدام خادم الترحيل فقط إذا لزم الأمر. يمكن لطرف آخر مراقبة عنوان IP الخاص بك.</string> + <string name="save_and_notify_contact">حفظ وإشعار جهة الاتصال</string> + <string name="settings_restart_app">إعادة التشغيل</string> + <string name="share_text_received_at">استلمت في: %s</string> + <string name="renegotiate_encryption">إعادة تفاوض التشفير</string> + <string name="sender_at_ts">%s في %s</string> + <string name="save_group_profile">حفظ ملف المجموعة</string> + <string name="color_secondary">ثانوي</string> + <string name="v5_1_self_destruct_passcode">كلمة مرور التدمير الذاتي</string> + <string name="sending_files_not_yet_supported">إرسال الملفات غير مدعوم بعد</string> + <string name="sender_cancelled_file_transfer">قام المرسل بإلغاء إرسال الملف</string> + <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(امسح أو ألصق)</string> + <string name="network_option_seconds_label">ثانية</string> + <string name="sender_may_have_deleted_the_connection_request">قد يكون المرسل قد ألغى طلب الاتصال</string> + <string name="scan_QR_code">مسح رمز الاستجابة السريعة</string> + <string name="send_us_an_email">أرسل لنا بريداً</string> + <string name="scan_code_from_contacts_app">مسح رمز الأمان من تطبيق جهة الاتصال</string> + <string name="share_invitation_link">مشاركة رابط ذو استخدام واحد</string> + <string name="stop_snd_file__message">إرسال الملف سوف يتوقف.</string> + <string name="icon_descr_send_message">إرسال رسالة</string> + <string name="send_disappearing_message_send">إرسال</string> + <string name="send_live_message">إرسال رسالة حية</string> + <string name="smp_servers_test_failed">فشلت تجربة الخادم!</string> + <string name="save_passphrase_in_keychain">حفظ كلمة المرور في مخزن المفاتيح</string> + <string name="button_send_direct_message">أرسل رسالة مباشرة</string> + <string name="sending_via">إرسال عبر</string> + <string name="conn_stats_section_title_servers">الخوادم</string> + <string name="v4_2_security_assessment">تقييم الأمان</string> + <string name="v4_4_disappearing_messages_desc">الرسائل المرسلة سيتم حذفها بعد المدة المحددة.</string> + <string name="v4_6_group_welcome_message_descr">تعيين رسالة تظهر للأعضاء الجدد!</string> + <string name="set_passcode">تعيين كلمة المرور</string> + <string name="share_text_sent_at">تم إرساله في: %s</string> + <string name="current_version_timestamp">%s (الحالي)</string> + <string name="color_sent_message">رسالة مرسلة</string> + <string name="set_group_preferences">تعيين إعدادت المجموهة</string> + <string name="v5_0_app_passcode_descr">عيينها بدلا من توثيق النظام</string> + <string name="share_verb">مشاركة</string> + <string name="send_verb">إرسال</string> + <string name="save_passphrase_and_open_chat">حفظ كلمة المرور وفتح الدردشة</string> + <string name="select_contacts">اختيار جهات اتصال</string> + <string name="accept_feature_set_1_day">تعيين يوم واحد</string> + <string name="custom_time_unit_seconds">ثواني</string> + <string name="sent_message">رسالة مرسلة</string> + <string name="send_disappearing_message">أرسل رسالة مؤقتة</string> + <string name="save_profile_password">حفظ كلمة مرور الحساب</string> + <string name="self_destruct">تدمير ذاتي</string> + <string name="scan_code">مسح الكود</string> + <string name="chat_with_the_founder">إرسال أسئلة وأفكار</string> + <string name="share_address_with_contacts_question">مشاركة العنوان مع جهات الاتصال</string> + <string name="share_address">مشاركة العنوان</string> + <string name="save_welcome_message_question">حفظ رسالة الترحيب؟</string> + <string name="smp_servers_save">حفظ السيرفرات</string> + <string name="settings_section_title_delivery_receipts">أرسل تقارير الاستلام إلى</string> + <string name="receipts_contacts_override_disabled">إرسال تقارير الاستلام معطل لـ %d جهة اتصال.</string> + <string name="receipts_contacts_override_enabled">إرسال تقارير الاستلام مفعل لـ %d جهة اتصال</string> + <string name="set_password_to_export">تعيين كلمة المرور للتصدير</string> + <string name="rcv_conn_event_verification_code_reset">تم تغيير رمز الأمان</string> + <string name="send_receipts">تقارير الارسال</string> + <string name="info_row_sent_at">تم إرساله في</string> + <string name="custom_time_picker_select">اختيار</string> + <string name="sending_delivery_receipts_will_be_enabled">إرسال تقارير الاستلام سيتم تفعيله لجميع جهات الاتصال.</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">إرسال تقارير الاستلام سيتم تفعيله لجميع جهات الاتصال ذات حسابات الدردشة الظاهرة</string> + <string name="smp_server_test_secure_queue">قائمة انتظار آمنة</string> + <string name="icon_descr_sent_msg_status_send_failed">فشل الإرسال</string> + <string name="icon_descr_sent_msg_status_sent">تم الإرسال</string> + <string name="text_field_set_contact_placeholder">تعيين اسم جهة الاتصال…</string> + <string name="send_live_message_desc">إرسال رسالة حية - سيتم تحديثها للمستلم مع كتابتك لها</string> + <string name="set_contact_name">تعيين اسم جهة الاتصال</string> + <string name="icon_descr_settings">الإعدادات</string> + <string name="smp_save_servers_question">حفظ السيرفرات؟</string> + <string name="smp_servers_scan_qr">مسح رمز الاستجابة السريعة للسيرفر</string> + <string name="security_code">رمز الأمان</string> + <string name="save_preferences_question">حفظ الإعدادات؟</string> + <string name="save_settings_question">حفظ الإعدادات؟</string> + <string name="secret_text">سري</string> + <string name="self_destruct_passcode">كلمة مرور التدمير الذاتي</string> + <string name="self_destruct_passcode_changed">تم تغيير كلمة مرور التدمير الذاتي!</string> + <string name="self_destruct_passcode_enabled">تم تفعيل كلمة مرور التدمير الذاتي</string> + <string name="settings_section_title_settings">الإعدادات</string> + <string name="simplex_link_invitation">دعوة لمرة واحدة SimpleX</string> + <string name="notification_preview_mode_message_desc">عرض جهة الاتصال والرسالة</string> + <string name="la_notice_title_simplex_lock">قفل SimpleX</string> + <string name="is_not_verified">لم يتحقق من %s</string> + <string name="chat_lock">قفل SimpleX</string> + <string name="smp_servers">خوادم SMP</string> + <string name="share_image">مشاركة الوسائط…</string> + <string name="ntf_channel_messages">رسائل SimpleX Chat</string> + <string name="lock_not_enabled">لم يتم تمكين قفل SimpleX!</string> + <string name="auth_stop_chat">إيقاف الدردشة</string> + <string name="stop_rcv_file__title">التوقف عن استلام الملف؟</string> + <string name="share_file">مشاركة الملف…</string> + <string name="image_descr_simplex_logo">شعار SimpleX</string> + <string name="icon_descr_simplex_team">فريق SimpleX</string> + <string name="show_QR_code">عرض رمز QR</string> + <string name="is_verified">%s تم التحقق منه</string> + <string name="smp_servers_test_some_failed">فشلت بعض الخوادم في الاختبار:</string> + <string name="send_link_previews">إرسال معاينات الارتباط</string> + <string name="skip_inviting_button">تخطي دعوة الأعضاء</string> + <string name="stop_chat_question">إيقاف الدردشة؟</string> + <string name="show_call_on_lock_screen">عرض</string> + <string name="non_fatal_errors_occured_during_import">حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد - قد ترى وحدة تحكم الدردشة لمزيد من التفاصيل.</string> + <string name="settings_section_title_socks">وكيل SOCKS</string> + <string name="v4_2_security_assessment_desc">تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits.</string> + <string name="stop_chat_confirmation">إيقاف</string> + <string name="settings_notification_preview_mode_title">عرض المعاينة</string> + <string name="icon_descr_speaker_off">السماعة متوقفة</string> + <string name="la_lock_mode">وضع قفل SimpleX</string> + <string name="share_link">مشاركة الرابط</string> + <string name="alert_title_skipped_messages">الرسائل التي تم تخطيها</string> + <string name="simplex_address">عنوان SimpleX</string> + <string name="theme_simplex">SimpleX</string> + <string name="simplex_link_contact">عنوان جهة أتصال SimpleX</string> + <string name="simplex_link_group">رابط مجموعة SimpleX</string> + <string name="simplex_link_mode">روابط SimpleX</string> + <string name="share_message">مشاركة الرسالة…</string> + <string name="add_contact_or_create_group">ابدأ محادثة جديدة</string> + <string name="star_on_github">اضع نجمة على GitHub</string> + <string name="stop_sharing_address">إيقاف مشاركة العنوان؟</string> + <string name="stop_sharing">إيقاف المشاركة</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">أوقف الدردشة لتصدير أو استيراد أو حذف قاعدة بيانات الدردشة. لن تتمكن من تلقي الرسائل وإرسالها أثناء إيقاف الدردشة.</string> + <string name="stop_chat_to_enable_database_actions">أوقف الدردشة لتمكين إجراءات قاعدة البيانات.</string> + <string name="chat_item_ttl_seconds">%s ثانية/ثواني</string> + <string name="callstate_starting">يبدأ…</string> + <string name="auth_simplex_lock_turned_on">تم تشغيل القفل SimpleX</string> + <string name="show_dev_options">عرض:</string> + <string name="show_developer_options">عرض خيارات المطور</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="error_smp_test_server_auth">يتطلب الخادم إذنًا لإنشاء قوائم انتظار، تحقق من كلمة المرور</string> + <string name="error_xftp_test_server_auth">يتطلب الخادم إذنًا للتحميل، تحقق من كلمة المرور</string> + <string name="notification_preview_mode_contact_desc">عرض جهة الاتصال فقط</string> + <string name="ntf_channel_calls">مكالمات SimpleX Chat</string> + <string name="simplex_service_notification_title">خدمة SimpleX Chat</string> + <string name="notifications_mode_periodic">يبدأ بشكل دوري</string> + <string name="stop_file__confirm">إيقاف</string> + <string name="stop_file__action">إيقاف الملف</string> + <string name="stop_snd_file__title">التوقف عن إرسال الملف؟</string> + <string name="icon_descr_address">عنوان SimpleX</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string> + <string name="share_with_contacts">مشاركة مع جهات الاتصال</string> + <string name="shutdown_alert_question">إيقاف التشغيل؟</string> + <string name="network_socks_proxy_settings">إعدادات وكيل SOCKS</string> + <string name="settings_shutdown">إيقاف التشغيل</string> + <string name="icon_descr_speaker_on">السماعة قيد التشغيل</string> + <string name="submit_passcode">إرسال</string> + <string name="language_system">النظام</string> + <string name="theme">السمة</string> + <string name="to_start_a_new_chat_help_header">لبدء محادثة جديدة</string> + <string name="to_verify_compare">للتحقق من التشفير من طرف إلى طرف مع جهة الاتصال الخاصة بك، قارن (أو امسح) الرمز الموجود على أجهزتك.</string> + <string name="group_is_decentralized">المجموعة لامركزية بالكامل - فهي مرئية فقط للأعضاء.</string> + <string name="theme_system">النظام</string> + <string name="error_smp_test_failed_at_step">فشل الاختبار في الخطوة %s.</string> + <string name="v4_5_italian_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="periodic_notifications_desc">يجلب التطبيق الرسائل الجديدة بشكل دوري - يستخدم نسبة قليلة من البطارية يوميًا. لا يستخدم التطبيق إشعارات الدفع - لا يتم إرسال البيانات من جهازك إلى الخوادم.</string> + <string name="connection_you_accepted_will_be_cancelled">سيتم إلغاء الاتصال الذي قبلته!</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">لن تتمكن جهة الاتصال التي شاركت هذا الرابط معها من الاتصال!</string> + <string name="this_text_is_available_in_settings">هذا النص متاح في الإعدادات</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">لحماية الخصوصية، بدلاً من معرفات المستخدم التي تستخدمها جميع الأنظمة الأساسية الأخرى, يحتوي SimpleX على معرفات لقوائم انتظار الرسائل، منفصلة لكل جهة من جهات اتصالك.</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">لحماية معلوماتك، قم بتشغيل قفل SimpleX +\nسيُطلب منك إكمال المصادقة قبل تمكين هذه الميزة.</string> + <string name="network_session_mode_transport_isolation">عزل النقل</string> + <string name="v4_4_french_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="v4_6_audio_video_calls_descr">دعم البلوتوث وتحسينات أخرى.</string> + <string name="v5_0_polish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[للحفاظ على خصوصيتك، بدلاً من دفع الإشعارات، يحتوي التطبيق على <b>خدمة SimpleX تعمل في الخلفية</b> – يستخدم نسبة قليلة من البطارية يوميًا.]]></string> + <string name="tap_to_start_new_chat">انقر لبدء محادثة جديدة</string> + <string name="to_share_with_your_contact">(للمشاركة مع جهة اتصالك)</string> + <string name="to_connect_via_link_title">للتواصل عبر الرابط</string> + <string name="scan_qr_to_connect_to_contact">للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق.</string> + <string name="smp_servers_test_servers">خوادم الاختبار</string> + <string name="first_platform_without_user_ids">المنصة الأولى بدون أي معرفات للمستخدم - صمّمناه ليكون خاصًا.</string> + <string name="settings_section_title_support">دعم SIMPLEX CHAT</string> + <string name="switch_verb">تبديل</string> + <string name="color_title">العنوان الرئيسي</string> + <string name="moderate_message_will_be_marked_warning">سيتم وضع علامة على الرسالة على أنها تحت الإشراف لجميع الأعضاء.</string> + <string name="group_invitation_tap_to_join">انقر للانضمام</string> + <string name="to_reveal_profile_enter_password">للكشف عن ملف التعريف المخفي الخاص بك، أدخل كلمة مرور كاملة في حقل البحث في صفحة ملفات تعريف الدردشة الخاصة بك.</string> + <string name="group_invitation_tap_to_join_incognito">انقر للانضمام إلى وضع التخفي</string> + <string name="la_mode_system">النظام</string> + <string name="settings_section_title_themes">السمات</string> + <string name="v4_6_chinese_spanish_interface_descr">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="database_initialization_error_desc">قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد</string> + <string name="theme_colors_section_title">ألوان السمة</string> + <string name="tap_to_activate_profile">انقر لتنشيط الملف الشخصي.</string> + <string name="should_be_at_least_one_profile">يجب أن يكون هناك ملف تعريف مستخدم واحد على الأقل.</string> + <string name="v4_5_transport_isolation">عزل النقل</string> + <string name="this_string_is_not_a_connection_link">هذه السلسلة ليست رابط اتصال!</string> + <string name="receipts_section_description">هذه الإعدادات لملف التعريف الحالي الخاص بك</string> + <string name="receipts_section_description_1">يمكن تجاوزها في إعدادات الاتصال و المجموعة.</string> + <string name="network_option_tcp_connection_timeout">انتهت مهلة اتصال TCP</string> + <string name="v4_5_private_filenames_descr">لحماية المنطقة الزمنية، تستخدم ملفات الصور / الصوت التوقيت العالمي المنسق (UTC).</string> + <string name="v5_2_message_delivery_receipts_descr">فقدنا القراد الثاني! ✅</string> + <string name="whats_new_thanks_to_users_contribute_weblate">بفضل المستخدمين - المساهمة عبر Weblate!</string> + <string name="database_backup_can_be_restored">لم تكتمل محاولة تغيير عبارة مرور قاعدة البيانات.</string> + <string name="enter_passphrase_notification_desc">لتلقي الإخطارات، يرجى إدخال عبارة مرور قاعدة البيانات</string> + <string name="should_be_at_least_one_visible_profile">يجب أن يكون هناك ملف تعريف مستخدم مرئي واحد على الأقل.</string> + <string name="la_lock_mode_system">مصادقة النظام</string> + <string name="sync_connection_force_desc">يعمل التشفير واتفاقية التشفير الجديدة غير مطلوبة. قد ينتج عن ذلك أخطاء في الاتصال!</string> + <string name="image_decoding_exception_desc">لا يمكن فك ترميز الصورة. من فضلك، جرب صورة مختلفة أو تواصل مع المطورين.</string> + <string name="moderate_message_will_be_deleted_warning">سيتم حذف الرسالة لجميع الأعضاء.</string> + <string name="images_limit_title">الصور كثيرة!</string> + <string name="videos_limit_title">مقاطع الفيديو كثيرة!</string> + <string name="chat_help_tap_button">زر النقر</string> + <string name="thank_you_for_installing_simplex">شكرًا لك على تثبيت SimpleX Chat!</string> + <string name="smp_servers_test_server">خادم الاختبار</string> + <string name="alert_text_msg_bad_hash">تجزئة الرسالة السابقة مختلفة.</string> + <string name="alert_text_msg_bad_id">معرف الرسالة التالية غير صحيح (أقل أو يساوي السابق). +\nيمكن أن يحدث ذلك بسبب بعض العلل أو عندما يُخترق الاتصال.</string> + <string name="unfavorite_chat">إزالة من المفضلة</string> + <string name="trying_to_connect_to_server_to_receive_messages">محاولة الاتصال بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه.</string> + <string name="choose_file_title">اختيار ملف</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">إرسال غير مصرح به</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">محاولة الاتصال بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه (خطأ: %1$s).</string> + <string name="la_notice_turn_on">تشغيل</string> + <string name="webrtc_ice_servers">خوادم WebRTC ICE</string> + <string name="alert_title_cant_invite_contacts_descr">أنت تستخدم ملفًا شخصيًا متخفيًا لهذه المجموعة - لمنع مشاركة ملفك الشخصي الرئيسي الذي يدعو جهات الاتصال غير مسموح به</string> + <string name="snd_group_event_changed_member_role">غيّرتَ دور %s إلى %s</string> + <string name="chat_preferences_yes">نعم</string> + <string name="connected_to_server_to_receive_messages_from_contact">أنت متصل بالخادم المستخدم لتلقي الرسائل من جهة الاتصال هذه.</string> + <string name="sender_you_pronoun">أنت</string> + <string name="description_you_shared_one_time_link">لقد شاركت رابط لمرة واحدة</string> + <string name="profile_will_be_sent_to_contact_sending_link">سيتم إرسال ملف التعريفك إلى جهة الاتصال التي تلقيت منها هذا الارتباط.</string> + <string name="you_will_join_group">ستنضم إلى مجموعة يشير إليها هذا الرابط وتتصل بأعضائها.</string> + <string name="your_chat_profiles">ملفات تعريف الدردشة الخاصة بك</string> + <string name="your_simplex_contact_address">عنوان SimpleX الخاص بك</string> + <string name="your_SMP_servers">خوادم SMP الخاصة بك</string> + <string name="update_onion_hosts_settings_question">هل تريد تحديث إعداد مضيفي onion.؟</string> + <string name="onboarding_notifications_mode_off">عندما يكون التطبيق قيد التشغيل</string> + <string name="call_connection_via_relay">عبر المُرحل</string> + <string name="you_joined_this_group">لقد انضممت إلى هذه المجموعة</string> + <string name="you_rejected_group_invitation">لقد رفضت دعوة المجموعة</string> + <string name="incognito_info_share">عندما تشارك ملفًا شخصيًا متخفيًا مع شخص ما، فسيتم استخدام هذا الملف الشخصي للمجموعات التي يدعوك إليها.</string> + <string name="failed_to_create_user_duplicate_desc">لديك بالفعل ملف تعريف دردشة بنفس اسم العرض. الرجاء اختيار اسم آخر.</string> + <string name="you_are_already_connected_to_vName_via_this_link">أنت متصل بالفعل بـ%1$s.</string> + <string name="waiting_for_video">في انتظار الفيديو</string> + <string name="video_will_be_received_when_contact_completes_uploading">سيتم استلام الفيديو عند اكتمال تحميل جهة اتصالك.</string> + <string name="verify_security_code">تحقق من رمز الحماية</string> + <string name="v4_3_voice_messages">رسائل صوتية</string> + <string name="you_can_accept_or_reject_connection">عندما يطلب الأشخاص الاتصال، يمكنك قبوله أو رفضه.</string> + <string name="you_will_be_connected_when_group_host_device_is_online">سوف تكون متصلاً بالمجموعة عندما يكون جهاز مضيف المجموعة متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">سوف تكون متصلاً عندما يتم قبول طلب الاتصال الخاص بك، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="using_simplex_chat_servers">تستخدم خوادم SimpleX Chat.</string> + <string name="network_socks_toggle_use_socks_proxy">استخدم وكيل SOCKS</string> + <string name="network_use_onion_hosts">استخدم مضيفي onion.</string> + <string name="network_enable_socks">استخدام وكيل SOCKS؟</string> + <string name="network_use_onion_hosts_prefer">عندما تكون متاحة</string> + <string name="your_contacts_will_remain_connected">ستبقى جهات اتصالك متصلة.</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">لا نقوم بتخزين أي من جهات الاتصال أو الرسائل الخاصة بك (بمجرد تسليمها) على الخوادم.</string> + <string name="you_can_use_markdown_to_format_messages__prompt">يمكنك استخدام تخفيض السعر لتنسيق الرسائل:</string> + <string name="use_chat">استخدم الدردشة</string> + <string name="settings_section_title_you">أنت</string> + <string name="unknown_error">خطأ غير معروف</string> + <string name="snd_group_event_changed_role_for_yourself">غيّرتَ دور نفسك إلى %s</string> + <string name="you_sent_group_invitation">لقد أرسلت دعوة جماعية</string> + <string name="your_preferences">تفضيلاتك</string> + <string name="v4_4_verify_connection_security">تحقق من أمان الاتصال</string> + <string name="you_can_turn_on_lock">يمكنك تفعيل قفل SimpleX عبر الإعدادات.</string> + <string name="you_have_no_chats">ليس لديك أي محادثات</string> + <string name="icon_descr_video_snd_complete">أرسلت الفيديو</string> + <string name="voice_messages_prohibited">الرسائل الصوتية ممنوعة!</string> + <string name="callstate_waiting_for_answer">بانتظار الرد…</string> + <string name="waiting_for_image">في انتظار الصورة</string> + <string name="waiting_for_file">في انتظار الملف</string> + <string name="view_security_code">عرض رمز الأمان</string> + <string name="you_wont_lose_your_contacts_if_delete_address">لن تفقد جهات اتصالك إذا حذفت عنوانك لاحقًا.</string> + <string name="your_XFTP_servers">خوادم XFTP الخاصة بك</string> + <string name="use_simplex_chat_servers__question">استخدم خوادم SimpleX Chat</string> + <string name="xftp_servers">خوادم XFTP</string> + <string name="your_ICE_servers">خوادم ICE الخاصة بك</string> + <string name="network_disable_socks">هل تستخدم اتصالاً مباشرًا بالإنترنت؟</string> + <string name="you_control_your_chat">أنت تتحكم في الدردشة!</string> + <string name="your_calls">مكالماتك</string> + <string name="wrong_passphrase_title">عبارة مرور خاطئة!</string> + <string name="database_downgrade_warning">تحذير: قد تفقد بعض البيانات!</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">يمكنك مشاركة رابط أو رمز QR - سيتمكن أي شخص من الانضمام إلى المجموعة. لن تفقد أعضاء المجموعة إذا حذفتها لاحقًا.</string> + <string name="group_welcome_title">رسالة الترحيب</string> + <string name="voice_messages">رسائل صوتية</string> + <string name="v4_3_irreversible_message_deletion_desc">يمكن أن تسمح جهات اتصالك بحذف الرسائل بالكامل.</string> + <string name="unknown_message_format">تنسيق رسالة غير معروف</string> + <string name="description_via_one_time_link">عبر رابط لمرة واحدة</string> + <string name="video_call_no_encryption">مكالمة الفيديو ليست مشفرة بين الطريفين</string> + <string name="snd_conn_event_switch_queue_phase_completed">غيّرتَ العنوان</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">سوف تكون متصلاً عندما يكون جهاز جهة الاتصال الخاصة بك متصلاً بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="snd_group_event_user_left">غادرت</string> + <string name="you_must_use_the_most_recent_version_of_database">يجب عليك استخدام أحدث إصدار من قاعدة بيانات الدردشة الخاصة بك على جهاز واحد فقط، وإلا فقد تتوقف عن تلقي الرسائل من بعض جهات الاتصال.</string> + <string name="video_will_be_received_when_contact_is_online">سيتم استلام الفيديو عندما تكون جهة اتصالك متصلة بالإنترنت، يرجى الانتظار أو التحقق لاحقًا!</string> + <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[يمكنك التحكم من خلال الخادم (الخوادم) <b>لتلقي</b> الرسائل وجهات اتصالك - الخوادم التي تستخدمها لمراسلتهم.]]></string> + <string name="you_can_share_this_address_with_your_contacts">يمكنك مشاركة هذا العنوان مع جهات اتصالك للسماح لهم بالاتصال بـ%s.</string> + <string name="snd_group_event_member_deleted">أُزيلت %1$s</string> + <string name="update_database">تحديث</string> + <string name="database_is_not_encrypted">قاعدة بيانات الدردشة الخاصة بك غير مشفرة - قم بتعيين عبارة المرور لحمايتها.</string> + <string name="wrong_passphrase">عبارة مرور قاعدة بيانات خاطئة</string> + <string name="group_main_profile_sent">سيتم إرسال ملف تعريف الدردشة الخاص بك إلى أعضاء المجموعة</string> + <string name="personal_welcome">مرحبًا! %1$s</string> + <string name="contact_wants_to_connect_with_you">يريد الاتصال بك!</string> + <string name="your_ice_servers">خوادم ICE الخاصة بك</string> + <string name="your_privacy">خصوصيتك</string> + <string name="rcv_group_event_updated_group_profile">حدثت ملف تعريف المجموعة</string> + <string name="group_info_member_you">أنت: %1$s</string> + <string name="update_network_settings_confirmation">تحديث</string> + <string name="update_network_settings_question">تحديث إعدادات الشبكة؟</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">سيؤدي تحديث الإعدادات إلى إعادة توصيل العميل بجميع الخوادم.</string> + <string name="you_are_observer">أنت المراقب</string> + <string name="you_are_invited_to_group">أنت مدعو إلى المجموعة</string> + <string name="callstate_waiting_for_confirmation">في انتظار التأكيد…</string> + <string name="unknown_database_error_with_info">خطأ غير معروف في قاعدة البيانات: %s</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">يمكنك بدء الدردشة عبر إعدادات التطبيق / قاعدة البيانات أو عن طريق إعادة تشغيل التطبيق.</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">سيُطلب منك المصادقة عند بدء تشغيل التطبيق أو استئنافه بعد 30 ثانية في الخلفية.</string> + <string name="voice_message_with_duration">رسالة صوتية (%1$s)</string> + <string name="your_settings">إعداداتك</string> + <string name="update_network_session_mode_question">تحديث وضع عزل النقل؟</string> + <string name="your_profile_is_stored_on_your_device">يُخزن ملف التعريف وجهات الاتصال والرسائل التي سلُمت على جهازك.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">سيتم حذف قاعدة بيانات الدردشة الحالية واستبدالها بالقاعدة المستوردة. +\nلا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي.</string> + <string name="update_database_passphrase">تحديث عبارة مرور قاعدة البيانات</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">سوف تتوقف عن تلقي الرسائل من هذه المجموعة. سيتم الاحتفاظ سجل الدردشة.</string> + <string name="custom_time_unit_weeks">أسابيع</string> + <string name="you_can_hide_or_mute_user_profile">يمكنك إخفاء أو كتم ملف تعريف المستخدم - اضغط مطولاً للقائمة.</string> + <string name="whats_new">ما هو الجديد</string> + <string name="your_current_profile">ملفك الشخصي الحالي</string> + <string name="simplex_link_connection">عبر %1$s</string> + <string name="icon_descr_received_msg_status_unread">غير مقروءة</string> + <string name="welcome">مرحبًا!</string> + <string name="icon_descr_waiting_for_image">في انتظار الصورة</string> + <string name="video_descr">فيديو</string> + <string name="icon_descr_waiting_for_video">في انتظار الفيديو</string> + <string name="gallery_video_button">فيديو</string> + <string name="you_can_share_your_address">يمكنك مشاركة عنوانك كرابط أو رمز QR - يمكن لأي شخص الاتصال بك.</string> + <string name="you_can_create_it_later">يمكنك إنشاؤه لاحقًا</string> + <string name="your_contacts_will_see_it">سوف تراها جهات اتصالك في whatsapp. +\nيمكنك تغييره في الإعدادات.</string> + <string name="invite_prohibited_description">أنت تحاول دعوة جهة اتصال قمت بمشاركة ملف تعريف متخفي معها إلى المجموعة التي تستخدم فيها ملفك الشخصي الرئيسي</string> + <string name="user_unmute">إلغاء الكتم</string> + <string name="unmute_chat">إلغاء الكتم</string> + <string name="you_accepted_connection">لقد قبلت الاتصال</string> + <string name="unhide_profile">إلغاء إخفاء ملف تعريف</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">يجب أن تكون جهة الاتصال متصلة بالإنترنت حتى يكتمل الاتصال. +\nيمكنك إلغاء هذا الاتصال وإزالة جهة الاتصال (والمحاولة لاحقًا باستخدام رابط جديد).</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[يمكنك أيضًا الاتصال بالضغط على الرابط. إذا تم فتحه في المتصفح، فانقر فوق الزر <b>فتح في تطبيق الجوال</b>.]]></string> + <string name="smp_servers_use_server_for_new_conn">استخدم للاتصالات الجديدة</string> + <string name="smp_servers_use_server">استخدم الخادم</string> + <string name="smp_servers_your_server_address">عنوان خادمك</string> + <string name="your_chat_database">قاعدة بيانات الدردشة الخاصة بك</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">أنت مدعو إلى المجموعة. انضم للتواصل مع أعضاء المجموعة.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">لقد انضممت إلى هذه المجموعة. الاتصال بدعوة عضو المجموعة.</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">غيّرتَ العنوان ل%s</string> + <string name="unhide_chat_profile">إلغاء إخفاء ملف تعريف الدردشة</string> + <string name="voice_prohibited_in_this_chat">الرسائل الصوتية ممنوعة في هذه الدردشة.</string> + <string name="v5_0_large_files_support">مقاطع فيديو وملفات تصل إلى 1 جيجا بايت</string> + <string name="v5_1_better_messages_descr">- رسائل صوتية تصل إلى 5 دقائق. +\n- الوقت المخصص لتختفي. +\n- تحرير التاريخ.</string> + <string name="you_can_enable_delivery_receipts_later">يمكنك تفعيلة لاحقًا عبر الإعدادات</string> + <string name="you_can_enable_delivery_receipts_later_alert">يمكنك تمكينها لاحقًا عبر إعدادات الخصوصية والأمان للتطبيق.</string> + <string name="description_via_group_link">عبر رابط المجموعة</string> + <string name="description_you_shared_one_time_link_incognito">لقد شاركت رابط لمرة واحدة متخفي</string> + <string name="simplex_link_mode_browser">عبر المتصفح</string> + <string name="you_have_to_enter_passphrase_every_time">يجب عليك إدخال عبارة المرور في كل مرة يبدأ فيها التطبيق - لا يتم تخزينها على الجهاز.</string> + <string name="upgrade_and_open_chat">قم بالترقية وفتح الدردشة</string> + <string name="button_welcome_message">رسالة الترحيب</string> + <string name="description_via_contact_address_link">عبر رابط عنوان الاتصال</string> + <string name="connection_error_auth_desc">ما لم يحذف جهة الاتصال الاتصال أو تم استخدام هذا الرابط بالفعل، فقد يكون خطأ - الرجاء الإبلاغ عنه. +\nللاتصال، يرجى مطالبة جهة اتصالك بإنشاء ارتباط اتصال آخر والتحقق من أن لديك اتصال شبكة ثابت.</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">سيتم إرسال ملف تعريف الدردشة الخاص بك +\nإلى جهة اتصالك</string> + <string name="user_unhide">إلغاء الإخفاء</string> + <string name="incognito_random_profile">ملفك الشخصي العشوائي</string> + <string name="you_will_still_receive_calls_and_ntfs">ستستمر في تلقي المكالمات والإشعارات من الملفات الشخصية المكتومة عندما تكون نشطة.</string> + <string name="chat_preferences_you_allow">انت تسمح بها</string> + <string name="icon_descr_video_call">مكالمة فيديو</string> + <string name="voice_messages_are_prohibited">الرسائل الصوتية ممنوعة في هذه الدردشة.</string> + <string name="auth_unlock">فتح القفل</string> + <string name="smp_server_test_upload_file">رفع الملف</string> + <string name="la_could_not_be_verified">لا يمكن التحقق منك؛ حاول مرة اخرى.</string> + <string name="voice_message">رسالة صوتية</string> + <string name="voice_message_send_text">رسالة صوتية…</string> + <string name="group_preview_you_are_invited">أنت مدعو إلى المجموعة</string> + <string name="observer_cant_send_message_title">لا يمكنك إرسال رسائل!</string> + <string name="you_need_to_allow_to_send_voice">تحتاج إلى السماح لجهة الاتصال الخاصة بك بإرسال رسائل صوتية لتتمكن من إرسالها.</string> + <string name="contact_sent_large_file">أرسلت جهة اتصالك ملفًا أكبر من الحجم الأقصى المعتمد حاليًا (%1$s).</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[يمكنك <font color=#0088ff>الاتصال بمطوري SimpleX Chat لطرح أي أسئلة وتلقي التحديثات</font>.]]></string> + <string name="smp_servers_your_server">خادمك</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">يُخزن ملف تعريفك على جهازك ومشاركته فقط مع جهات اتصالك. لا تستطيع خوادم SimpleX رؤية ملف تعريفك.</string> + <string name="icon_descr_video_off">الفيديو مقفل</string> + <string name="icon_descr_video_on">الفيديو مُشغَّل</string> + <string name="v4_2_auto_accept_contact_requests_desc">مع رسالة ترحيب اختيارية.</string> + <string name="in_developing_title">قريباً!</string> + <string name="in_developing_desc">هذه الميزة ليست مدعومة حتى الآن. جرب الإصدار التالي.</string> + <string name="receipts_groups_disable_keep_overrides">تعطيل (الاحتفاظ بتجاوزات المجموعة)</string> + <string name="receipts_groups_enable_for_all">تفعيل لجميع المجموعات</string> + <string name="receipts_groups_override_enabled">إرسال الإيصالات مفعّلة لـ%d مجموعات</string> + <string name="receipts_groups_disable_for_all">تعطيل لجميع المجموعات</string> + <string name="send_receipts_disabled_alert_title">الإيصالات مُعطَّلة</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled_alert_msg">تضم هذه المجموعة أكثر من %1$d عضو، ولا يتم إرسال إيصالات التسليم.</string> + <string name="delivery">التوصيل</string> + <string name="send_receipts_disabled">مُعطَّل</string> + <string name="receipts_groups_title_disable">تعطيل الإيصالات للمجموعات؟</string> + <string name="receipts_groups_enable_keep_overrides">تفعيل (الاحتفاظ بتجاوزات المجموعة)</string> + <string name="receipts_groups_title_enable">تفعيل الإيصالات للمجموعات؟</string> + <string name="no_info_on_delivery">لا توجد معلومات عن التسليم</string> + <string name="no_selected_chat">لا توجد دردشة محددة</string> + <string name="receipts_groups_override_disabled">إرسال الإيصالات مُعطَّلة لـ%d مجموعات</string> + <string name="receipts_section_groups">مجموعات صغيرة (الحد الأقصى 20)</string> + <string name="connect_via_member_address_alert_title">الاتصال مباشرةً؟</string> + <string name="connect_via_member_address_alert_desc">سيتم إرسال طلب الاتصال لعضو المجموعة هذا.</string> + <string name="connect_via_link_incognito">اتصال متخفي</string> + <string name="connect_use_current_profile">استخدم ملف التعريف الحالي</string> + <string name="disable_notifications_button">تعطيل الإشعارات</string> + <string name="turn_off_system_restriction_button">افتح إعدادات التطبيق</string> + <string name="system_restricted_background_desc">لا يمكن تشغيل SimpleX في الخلفية. ستتلقى الإشعارات فقط عندما يكون التطبيق قيد التشغيل.</string> + <string name="connect__a_new_random_profile_will_be_shared">سيتم مشاركة ملف تعريف عشوائي جديد.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">الصق الرابط الذي تلقيته للتواصل مع جهة الاتصال الخاصة بك…</string> + <string name="connect__your_profile_will_be_shared">ستتم مشاركة ملفك الشخصي %1$s.</string> + <string name="system_restricted_background_in_call_desc">قد يغلق التطبيق بعد دقيقة واحدة في الخلفية.</string> + <string name="turn_off_battery_optimization_button">سماح</string> + <string name="system_restricted_background_in_call_title">لا مكالمات في الخلفية</string> + <string name="system_restricted_background_warn"><![CDATA[لتمكين الإشعارات، يرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[لإجراء مكالمات في الخلفية، يرجى اختيار <b>استهلاك بطارية التطبيق</b> / <b>غير مقيد</b> في إعدادات التطبيق.]]></string> + <string name="connect_use_new_incognito_profile">استخدم ملف تعريف متخفي جديد</string> + <string name="you_invited_a_contact">أنت دعوت جهة اتصال</string> + <string name="privacy_message_draft">مسودة الرسالة</string> + <string name="rcv_group_event_2_members_connected">%s و %s متصل</string> + <string name="privacy_show_last_messages">إظهار الرسائل الأخيرة</string> + <string name="rcv_group_event_n_members_connected">%s، %s و %d أعضاء آخرين متصلون</string> + <string name="rcv_group_event_3_members_connected">%s، %s و %s متصل</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">سيتم تشفير قاعدة البيانات وتخزين عبارة المرور في الإعدادات.</string> + <string name="you_can_change_it_later">يُخزين عبارة المرور العشوائية في الإعدادات كنص عادي. +\nيمكنك تغييره لاحقا.</string> + <string name="database_encryption_will_be_updated_in_settings">سيتم تحديث عبارة مرور تشفير قاعدة البيانات وتخزينها في الإعدادات.</string> + <string name="remove_passphrase_from_settings">هل تريد إزالة عبارة المرور من الإعدادات؟</string> + <string name="use_random_passphrase">استخدم عبارة مرور عشوائية</string> + <string name="save_passphrase_in_settings">حفظ عبارة المرور في الإعدادات</string> + <string name="setup_database_passphrase">إعداد كلمة المرور لقاعدة البيانات</string> + <string name="set_database_passphrase">تعيين عبارة مرور قاعدة البيانات</string> + <string name="open_database_folder">افتح مجلد قاعدة البيانات</string> + <string name="passphrase_will_be_saved_in_settings">سيتم تخزين عبارة المرور في الإعدادات كنص عادي بعد تغييرها أو إعادة تشغيل التطبيق.</string> + <string name="settings_is_storing_in_clear_text">يُخزين عبارة المرور في الإعدادات كنص عادي.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>يُرجى الملاحظة</b>: يتم توصيل مرحلات الرسائل والملفات عبر وكيل SOCKS. تستخدم المكالمات وإرسال معاينات الارتباط الاتصال المباشر.]]></string> + <string name="encrypt_local_files">تشفير الملفات المحلية</string> + <string name="v5_3_encrypt_local_files">تشفير الملفات والوسائط المخزنة</string> + <string name="v5_3_new_desktop_app">تطبيق سطح المكتب الجديد!</string> + <string name="v5_3_new_interface_languages">6 لغات واجهة جديدة</string> + <string name="v5_3_encrypt_local_files_descr">يقوم التطبيق بتشفير الملفات المحلية الجديدة (باستثناء مقاطع الفيديو).</string> + <string name="v5_3_discover_join_groups">اكتشاف والانضمام إلى المجموعات</string> + <string name="v5_3_new_interface_languages_descr">العربية والبلغارية والفنلندية والعبرية والتايلاندية والأوكرانية - شكرًا للمستخدمين و Weblate.</string> + <string name="v5_3_new_desktop_app_descr">إنشاء ملف تعريف جديد في تطبيق سطح المكتب. 💻</string> + <string name="v5_3_discover_join_groups_descr">- الاتصال بخدمة الدليل (تجريبي)! +\n- إيصالات التسليم (ما يصل إلى 20 عضوا). +\n- أسرع وأكثر استقرارًا.</string> </resources> \ No newline at end of file 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 f3fd6f128b..ab0d943f33 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -7,9 +7,12 @@ <string name="connect_via_contact_link">Connect via contact link?</string> <string name="connect_via_invitation_link">Connect via invitation link?</string> <string name="connect_via_group_link">Connect via group link?</string> + <string name="connect_use_current_profile">Use current profile</string> + <string name="connect_use_new_incognito_profile">Use new incognito profile</string> <string name="profile_will_be_sent_to_contact_sending_link">Your profile will be sent to the contact that you received this link from.</string> <string name="you_will_join_group">You will join a group this link refers to and connect to its group members.</string> <string name="connect_via_link_verb">Connect</string> + <string name="connect_via_link_incognito">Connect incognito</string> <!-- MainActivity.kt --> <string name="opening_database">Opening database…</string> @@ -129,11 +132,19 @@ <string name="service_notifications_disabled">Instant notifications are disabled!</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[To preserve your privacy, instead of push notifications the app has a <b>SimpleX background service</b> – it uses a few percent of the battery per day.]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>It can be disabled via settings</b> – notifications will still be shown while the app is running.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[In order to use it, please <b>disable battery optimization</b> for SimpleX in the next dialog. Otherwise, the notifications will be disabled.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[To use it, please <b>allow SimpleX to run in background</b> in the next dialog. Otherwise, the notifications will be disabled.]]></string> <string name="turning_off_service_and_periodic">Battery optimization is active, turning off background service and periodic requests for new messages. You can re-enable them via settings.</string> <string name="periodic_notifications">Periodic notifications</string> <string name="periodic_notifications_disabled">Periodic notifications are disabled!</string> <string name="periodic_notifications_desc">The app fetches new messages periodically — it uses a few percent of the battery per day. The app doesn\'t use push notifications — data from your device is not sent to the servers.</string> + <string name="turn_off_battery_optimization_button">Allow</string> + <string name="turn_off_system_restriction_button">Open app settings</string> + <string name="disable_notifications_button">Disable notifications</string> + <string name="system_restricted_background_desc">SimpleX can\'t run in background. You will receive the notifications only when the app is running.</string> + <string name="system_restricted_background_warn"><![CDATA[To enable notifications, please choose <b>App battery usage</b> / <b>Unrestricted</b> in the app settings.]]></string> + <string name="system_restricted_background_in_call_title">No background calls</string> + <string name="system_restricted_background_in_call_desc">The app may be closed after 1 minute in background.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[To make calls in background, please choose <b>App battery usage</b> / <b>Unrestricted</b> in the app settings.]]></string> <string name="enter_passphrase_notification_title">Passphrase is needed</string> <string name="enter_passphrase_notification_desc">To receive notifications, please, enter the database passphrase</string> <string name="database_initialization_error_title">Can\'t initialize the database</string> @@ -222,6 +233,8 @@ <string name="edit_history">History</string> <string name="no_history">No history</string> <string name="in_reply_to">In reply to</string> + <string name="delivery">Delivery</string> + <string name="no_info_on_delivery">No delivery information</string> <string name="delete_verb">Delete</string> <string name="reveal_verb">Reveal</string> <string name="hide_verb">Hide</string> @@ -259,6 +272,7 @@ <string name="this_text_is_available_in_settings">This text is available in settings</string> <string name="your_chats">Chats</string> <string name="contact_connection_pending">connecting…</string> + <string name="member_contact_send_direct_message">send direct message</string> <string name="group_preview_you_are_invited">you are invited to group</string> <string name="group_preview_join_as">join as %s</string> <string name="group_connection_pending">connecting…</string> @@ -267,6 +281,9 @@ <string name="you_have_no_chats">You have no chats</string> <string name="no_filtered_chats">No filtered chats</string> + <!-- ChatView.kt --> + <string name="no_selected_chat">No selected chat</string> + <!-- ShareListView.kt --> <string name="share_message">Share message…</string> <string name="share_image">Share media…</string> @@ -288,6 +305,7 @@ <string name="observer_cant_send_message_desc">Please contact group admin.</string> <string name="files_and_media_prohibited">Files and media prohibited!</string> <string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string> + <string name="compose_send_direct_message_to_connect">Send direct message to connect</string> <!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt --> <string name="image_descr">Image</string> @@ -394,6 +412,7 @@ <string name="use_camera_button">Camera</string> <string name="from_gallery_button">From Gallery</string> <string name="choose_file">File</string> + <string name="choose_file_title">Choose a file</string> <string name="gallery_image_button">Image</string> <string name="gallery_video_button">Video</string> @@ -437,7 +456,7 @@ <!-- Pending contact connection alert dialogues --> - <string name="you_invited_your_contact">You invited your contact</string> + <string name="you_invited_a_contact">You invited a contact</string> <string name="you_accepted_connection">You accepted connection</string> <string name="delete_pending_connection__question">Delete pending connection?</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">The contact you shared this link with will NOT be able to connect!</string> @@ -483,11 +502,13 @@ <string name="your_chat_profile_will_be_sent_to_your_contact">Your chat profile will be sent\nto your contact</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[If you cannot meet in person, you can <b>scan QR code in the video call</b>, or your contact can share an invitation link.]]></string> <string name="share_invitation_link">Share 1-time link</string> - <string name="paste_connection_link_below_to_connect">Paste the link you received into the box below to connect with your contact.</string> - <string name="your_profile_will_be_sent">Your chat profile will be sent to your contact</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Paste the link you received to connect with your contact…</string> <string name="learn_more">Learn more</string> <string name="learn_more_about_address">About SimpleX address</string> + <string name="connect__a_new_random_profile_will_be_shared">A new random profile will be shared.</string> + <string name="connect__your_profile_will_be_shared">Your profile %1$s will be shared.</string> + <!-- Add Contact Learn More - AddContactLearnMore.kt --> <string name="scan_qr_to_connect_to_contact">To connect, your contact can scan QR code or use the link in the app.</string> <string name="if_you_cant_meet_in_person">If you can\'t meet in person, show QR code in a video call, or share the link.</string> @@ -596,7 +617,7 @@ <string name="network_use_onion_hosts_required">Required</string> <string name="network_use_onion_hosts_prefer_desc">Onion hosts will be used when available.</string> <string name="network_use_onion_hosts_no_desc">Onion hosts will not be used.</string> - <string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.</string> + <string name="network_use_onion_hosts_required_desc">Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion hosts will be used when available.</string> <string name="network_use_onion_hosts_no_desc_in_alert">Onion hosts will not be used.</string> <string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts will be required for connection.</string> @@ -607,6 +628,7 @@ <string name="network_session_mode_entity_description"><![CDATA[A separate TCP connection (and SOCKS credential) will be used <b>for each contact and group member</b>.\n<b>Please note</b>: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail.]]></string> <string name="update_network_session_mode_question">Update transport isolation mode?</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Set <i>Use .onion hosts</i> to No if SOCKS proxy does not support them.]]></string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Please note</b>: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]></string> <string name="appearance_settings">Appearance</string> <string name="customize_theme_title">Customize theme</string> <string name="theme_colors_section_title">THEME COLORS</string> @@ -750,6 +772,11 @@ <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Good for battery</b>. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Uses more battery</b>! Background service always runs – notifications are shown as soon as messages are available.]]></string> + <!-- SetupDatabasePassphrase.kt --> + <string name="setup_database_passphrase">Setup database passphrase</string> + <string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string> + <string name="use_random_passphrase">Use random passphrase</string> + <!-- MakeConnection --> <string name="paste_the_link_you_received">Paste received link</string> @@ -831,8 +858,11 @@ <string name="privacy_and_security">Privacy & security</string> <string name="your_privacy">Your privacy</string> <string name="protect_app_screen">Protect app screen</string> + <string name="encrypt_local_files">Encrypt local files</string> <string name="auto_accept_images">Auto-accept images</string> <string name="send_link_previews">Send link previews</string> + <string name="privacy_show_last_messages">Show last messages</string> + <string name="privacy_message_draft">Message draft</string> <string name="full_backup">App data backup</string> <string name="enable_lock">Enable lock</string> <string name="lock_mode">Lock mode</string> @@ -865,7 +895,7 @@ <string name="if_you_enter_passcode_data_removed">If you enter this passcode when opening the app, all app data will be irreversibly removed!</string> <string name="set_passcode">Set passcode</string> <string name="receipts_section_description">These settings are for your current profile</string> - <string name="receipts_section_description_1">They can be overridden in contact settings</string> + <string name="receipts_section_description_1">They can be overridden in contact and group settings.</string> <string name="receipts_section_contacts">Contacts</string> <string name="receipts_contacts_title_enable">Enable receipts?</string> <string name="receipts_contacts_title_disable">Disable receipts?</string> @@ -875,6 +905,15 @@ <string name="receipts_contacts_disable_keep_overrides">Disable (keep overrides)</string> <string name="receipts_contacts_enable_for_all">Enable for all</string> <string name="receipts_contacts_disable_for_all">Disable for all</string> + <string name="receipts_section_groups">Small groups (max 20)</string> + <string name="receipts_groups_title_enable">Enable receipts for groups?</string> + <string name="receipts_groups_title_disable">Disable receipts for groups?</string> + <string name="receipts_groups_override_enabled">Sending receipts is enabled for %d groups</string> + <string name="receipts_groups_override_disabled">Sending receipts is disabled for %d groups</string> + <string name="receipts_groups_enable_keep_overrides">Enable (keep group overrides)</string> + <string name="receipts_groups_disable_keep_overrides">Disable (keep group overrides)</string> + <string name="receipts_groups_enable_for_all">Enable for all groups</string> + <string name="receipts_groups_disable_for_all">Disable for all groups</string> <!-- Settings sections --> <string name="settings_section_title_you">YOU</string> @@ -909,6 +948,7 @@ <string name="import_database">Import database</string> <string name="new_database_archive">New database archive</string> <string name="old_database_archive">Old database archive</string> + <string name="open_database_folder">Open database folder</string> <string name="delete_database">Delete database</string> <string name="error_starting_chat">Error starting chat</string> <string name="stop_chat_question">Stop chat?</string> @@ -954,9 +994,11 @@ <!-- DatabaseEncryptionView.kt --> <string name="save_passphrase_in_keychain">Save passphrase in Keystore</string> + <string name="save_passphrase_in_settings">Save passphrase in settings</string> <string name="database_encrypted">Database encrypted!</string> <string name="error_encrypting_database">Error encrypting database</string> <string name="remove_passphrase_from_keychain">Remove passphrase from Keystore?</string> + <string name="remove_passphrase_from_settings">Remove passphrase from settings?</string> <string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string> <string name="remove_passphrase">Remove</string> <string name="encrypt_database">Encrypt</string> @@ -965,18 +1007,23 @@ <string name="new_passphrase">New passphrase…</string> <string name="confirm_new_passphrase">Confirm new passphrase…</string> <string name="update_database_passphrase">Update database passphrase</string> + <string name="set_database_passphrase">Set database passphrase</string> <string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string> <string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string> <string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string> + <string name="settings_is_storing_in_clear_text">The passphrase is stored in settings as plaintext.</string> <string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.]]></string> <string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string> + <string name="passphrase_will_be_saved_in_settings">The passphrase will be stored in settings as plaintext after you change it or restart the app.</string> <string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string> <string name="encrypt_database_question">Encrypt database?</string> <string name="change_database_passphrase_question">Change database passphrase?</string> <string name="database_will_be_encrypted">Database will be encrypted.</string> <string name="database_will_be_encrypted_and_passphrase_stored">Database will be encrypted and the passphrase stored in the Keystore.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Database will be encrypted and the passphrase stored in settings.</string> <string name="database_encryption_will_be_updated">Database encryption passphrase will be updated and stored in the Keystore.</string> + <string name="database_encryption_will_be_updated_in_settings">Database encryption passphrase will be updated and stored in settings.</string> <string name="database_passphrase_will_be_updated">Database encryption passphrase will be updated.</string> <string name="store_passphrase_securely">Please store passphrase securely, you will NOT be able to change it if you lose it.</string> <string name="store_passphrase_securely_without_recover">Please store passphrase securely, you will NOT be able to access chat if you lose it.</string> @@ -1069,12 +1116,19 @@ <string name="rcv_group_event_group_deleted">deleted group</string> <string name="rcv_group_event_updated_group_profile">updated group profile</string> <string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string> + <string name="rcv_group_event_member_created_contact">connected directly</string> <string name="snd_group_event_changed_member_role">you changed role of %s to %s</string> <string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string> <string name="snd_group_event_member_deleted">you removed %1$s</string> <string name="snd_group_event_user_left">you left</string> <string name="snd_group_event_group_profile_updated">group profile updated</string> + <string name="rcv_group_event_2_members_connected">%s and %s connected</string> + <string name="rcv_group_event_3_members_connected">%s, %s and %s connected</string> + <string name="rcv_group_event_n_members_connected">%s, %s and %d other members connected</string> + + <string name="rcv_group_event_open_chat">Open</string> + <!-- Conn event chat items --> <string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string> <string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string> @@ -1152,11 +1206,16 @@ <string name="error_creating_link_for_group">Error creating group link</string> <string name="error_updating_link_for_group">Error updating group link</string> <string name="error_deleting_link_for_group">Error deleting group link</string> + <string name="error_creating_member_contact">Error creating member contact</string> + <string name="error_sending_message_contact_invitation">Sending message contact invitation</string> <string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string> <string name="address_section_title">Address</string> <string name="share_address">Share address</string> <string name="you_can_share_this_address_with_your_contacts">You can share this address with your contacts to let them connect with %s.</string> <string name="send_receipts">Send receipts</string> + <string name="send_receipts_disabled">disabled</string> + <string name="send_receipts_disabled_alert_title">Receipts are disabled</string> + <string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string> <!-- Chat / Chat item info --> <string name="section_title_for_console">FOR CONSOLE</string> @@ -1179,6 +1238,7 @@ <string name="sender_at_ts">%s at %s</string> <string name="current_version_timestamp">%s (current)</string> <string name="item_info_no_text">no text</string> + <string name="recipient_colon_delivery_status">%s: %s</string> <!-- GroupMemberInfoView.kt --> <string name="button_remove_member">Remove member</string> @@ -1193,6 +1253,8 @@ <string name="change_member_role_question">Change group role?</string> <string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string> <string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string> + <string name="connect_via_member_address_alert_title">Connect directly?</string> + <string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string> <string name="error_removing_member">Error removing member</string> <string name="error_changing_role">Error changing role</string> <string name="info_row_group">Group</string> @@ -1226,7 +1288,6 @@ <string name="group_is_decentralized">The group is fully decentralized – it is visible only to the members.</string> <string name="group_display_name_field">Group display name:</string> <string name="group_full_name_field">Group full name:</string> - <string name="group_unsupported_incognito_main_profile_sent">Incognito mode is not supported here - your main profile will be sent to group members</string> <string name="group_main_profile_sent">Your chat profile will be sent to group members</string> @@ -1280,13 +1341,10 @@ <!-- Incognito mode --> <string name="incognito">Incognito</string> <string name="incognito_random_profile">Your random profile</string> - <string name="incognito_random_profile_description">A random profile will be sent to your contact</string> - <string name="incognito_random_profile_from_contact_description">A random profile will be sent to the contact that you received this link from</string> - <string name="incognito_info_protects">Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created.</string> + <string name="incognito_info_protects">Incognito mode protects your privacy by using a new random profile for each contact.</string> <string name="incognito_info_allows">It allows having many anonymous connections without any shared data between them in a single chat profile.</string> <string name="incognito_info_share">When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.</string> - <string name="incognito_info_find">To find the profile used for an incognito connection, tap the contact or group name on top of the chat.</string> <!-- Default themes --> <string name="theme_system">System</string> @@ -1501,6 +1559,16 @@ <string name="v5_2_disappear_one_message_descr">Even when disabled in the conversation.</string> <string name="v5_2_more_things">A few more things</string> <string name="v5_2_more_things_descr">- more stable message delivery.\n- a bit better groups.\n- and more!</string> + <string name="v5_3_new_desktop_app">New desktop app!</string> + <string name="v5_3_new_desktop_app_descr">Create new profile in desktop app. 💻</string> + <string name="v5_3_encrypt_local_files">Encrypt stored files & media</string> + <string name="v5_3_encrypt_local_files_descr">App encrypts new local files (except videos).</string> + <string name="v5_3_discover_join_groups">Discover and join groups</string> + <string name="v5_3_discover_join_groups_descr">- connect to directory service (BETA)!\n- delivery receipts (up to 20 members).\n- faster and more stable.</string> + <string name="v5_3_simpler_incognito_mode">Simplified incognito mode</string> + <string name="v5_3_simpler_incognito_mode_descr">Toggle incognito when connecting.</string> + <string name="v5_3_new_interface_languages">6 new interface languages</string> + <string name="v5_3_new_interface_languages_descr">Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian - thanks to the users and Weblate.</string> <!-- CustomTimePicker --> <string name="custom_time_unit_seconds">seconds</string> @@ -1522,4 +1590,8 @@ <string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string> <string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string> <string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string> -</resources> + + <!-- Under development --> + <string name="in_developing_title">Coming soon!</string> + <string name="in_developing_desc">This feature is not yet supported. Try the next release.</string> +</resources> \ No newline at end of file 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 188468a722..39b7bceee7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -19,7 +19,7 @@ <string name="add_address_to_your_profile">Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти.</string> <string name="color_secondary_variant">Допълнителен вторичен</string> <string name="users_add">Добави профил</string> - <string name="one_time_link_short">1-кратен линк</string> + <string name="one_time_link_short">Еднократен линк</string> <string name="chat_item_ttl_week">1 седмица</string> <string name="send_disappearing_message_5_minutes">5 минути</string> <string name="integrity_msg_skipped">%1$d пропуснато(и) съобщение(я)</string> @@ -29,7 +29,7 @@ <string name="group_info_section_title_num_members">%1$s ЧЛЕНОВЕ</string> <string name="abort_switch_receiving_address_question">Откажи смяна на адрес\?</string> <string name="abort_switch_receiving_address_confirm">Откажи</string> - <string name="abort_switch_receiving_address_desc">Промяната на адреса ще бъде прекъсната. Ще се използва стар адрес за получаване.</string> + <string name="abort_switch_receiving_address_desc">Промяната на адреса ще бъде прекъсната. Ще се използва старият адрес за получаване.</string> <string name="accept_contact_button">Приеми</string> <string name="accept_contact_incognito_button">Приеми инкогнито</string> <string name="about_simplex_chat">За SimpleX Chat</string> @@ -47,8 +47,8 @@ <string name="all_group_members_will_remain_connected">Всички членове на групата ще останат свързани.</string> <string name="accept_feature">Приеми</string> <string name="color_primary_variant">Допълнителен акцент</string> - <string name="v4_2_group_links_desc">Админите могат да създадат връзките за присъединяване към групи.</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Ако не можете да се срещнете лично, <b>покажете QR кода във видеоразговора</b> или споделете линка.</string> + <string name="v4_2_group_links_desc">Админите могат да създадат линкове за присъединяване към групи.</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Ако не можете да се срещнете лично, <b>покажете QR кода във видеоразговора</b> или споделете линка.]]></string> <string name="open_simplex_chat_to_accept_call">Отворете SimpleX Chat, за да приемете повикването</string> <string name="v4_6_audio_video_calls_descr">Поддръжка на bluetooth и други подобрения.</string> <string name="relay_server_protects_ip">Relay сървърът защитава вашия IP адрес, но може да наблюдава продължителността на разговора.</string> @@ -62,7 +62,7 @@ <string name="cannot_access_keychain">Не може да се осъществи достъп до Keystore, за да се запази паролата на базата данни</string> <string name="cannot_receive_file">Файлът не може да бъде получен</string> <string name="alert_title_cant_invite_contacts">Не може да поканят контактите!</string> - <string name="settings_notification_preview_title">Визуализация на известието</string> + <string name="settings_notification_preview_title">Визуализация на известията</string> <string name="notification_preview_mode_message">Текст на съобщението</string> <string name="notification_preview_new_message">ново съобщение</string> <string name="group_welcome_preview">Визуализация</string> @@ -84,12 +84,11 @@ <string name="empty_chat_profile_is_created">Създаен беше празен профил за чат с предоставеното име и приложението се отвари както обикновено.</string> <string name="notifications_mode_off_desc">Приложението може да получава известия само когато работи, няма да се стартира услуга във фонов режим</string> <string name="settings_section_title_icon">ИКОНА НА ПРИЛОЖЕНИЕТО</string> - <string name="incognito_random_profile_from_contact_description">Произволен профил ще бъде изпратен до контакта, от който сте получили тази връзка</string> <string name="la_authenticate">Идентифицирай</string> <string name="turning_off_service_and_periodic">Оптимизацията на батерията е активна, изключват се фоновата услуга и периодичните заявки за нови съобщения. Можете да ги активирате отново през настройките.</string> - <string name="network_session_mode_user_description">Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки чат профил, който имате в приложението</b>.</string> + <string name="network_session_mode_user_description"><![CDATA[Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки чат профил, който имате в приложението</b>.]]></string> <string name="icon_descr_audio_call">аудио разговор</string> - <string name="onboarding_notifications_mode_off_desc"><b>Най-добро за батерията</b>. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Най-добро за батерията</b>. Ще получавате известия само когато приложението работи (БЕЗ фонова услуга).]]></string> <string name="network_session_mode_entity_description">Ще се използва отделна TCP връзка (и идентификационни данни за SOCKS) <b>за всеки контакт и член на група</b>. \n<b>Моля, обърнете внимание</b>: ако имате много връзки, консумацията на батерията и трафика може да бъде значително по-висока и някои връзки може да се провалят.</string> <string name="icon_descr_asked_to_receive">Помолен да получи изображението</string> @@ -99,15 +98,15 @@ <string name="both_you_and_your_contacts_can_delete">И вие, и вашият контакт можете да изтриете необратимо изпратените съобщения.</string> <string name="auth_unavailable">Идентификацията е недостъпна</string> <string name="both_you_and_your_contact_can_add_message_reactions">И вие, и вашият контакт можете да добавяте реакции към съобщението.</string> - <string name="impossible_to_recover_passphrase"><b>Моля, обърнете внимание</b>: НЯМА да можете да възстановите или промените паролата, ако я загубите.</string> - <string name="onboarding_notifications_mode_service_desc"><b>Използва повече батерия</b>! Услугата на заден план винаги работи – известията се показват веднага щом съобщенията са налични.</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Моля, обърнете внимание</b>: НЯМА да можете да възстановите или промените паролата, ако я загубите.]]></string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Използва повече батерия</b>! Услугата на заден план винаги работи – известията се показват веднага щом съобщенията са налични.]]></string> <string name="integrity_msg_bad_hash">лош хеш на съобщението</string> <string name="alert_title_msg_bad_hash">Лош хеш на съобщението</string> <string name="no_call_on_lock_screen">Деактивиране</string> <string name="callstatus_calling">повикване…</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Ако не можете да се срещнете лично, можете <b>да сканирате QR код във видеоразговора</b> или вашият контакт може да сподели линк за покана.</string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Ако не можете да се срещнете лично, можете <b>да сканирате QR код във видеоразговора</b> или вашият контакт може да сподели линк за покана.]]></string> <string name="notifications_mode_service_desc">Услугата във фонов режим винаги работи – известията ще се показват веднага щом съобщенията са налични.</string> - <string name="it_can_disabled_via_settings_notifications_still_shown"><b>Може да бъде деактивирано през настройките</b> – известията ще продължат да се показват, докато приложението работи.</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Може да бъде деактивирано през настройките</b> – известията ще продължат да се показват, докато приложението работи.]]></string> <string name="ntf_channel_calls">SimpleX Chat разговори</string> <string name="notifications_mode_periodic">Започва периодично</string> <string name="database_initialization_error_title">Базата данни не може да се стартира</string> @@ -120,8 +119,8 @@ <string name="back">Назад</string> <string name="cancel_verb">Отказ</string> <string name="icon_descr_cancel_live_message">Спри живото съобщение</string> - <string name="add_new_contact_to_create_one_time_QR_code"><b>Добави нов контакт</b>: за да създадете своя еднократен QR код за вашия контакт.</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><b>Сканирай QR код</b>: за да се свържете с вашия контакт, който ви показва QR код.</string> + <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Добави нов контакт</b>: за да създадете своя еднократен QR код за вашия контакт.]]></string> + <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Сканирай QR код</b>: за да се свържете с вашия контакт, който ви показва QR код.]]></string> <string name="use_camera_button">Камера</string> <string name="if_you_cant_meet_in_person">Ако не можете да се срещнете лично, покажете QR код във видеоразговора или споделете линка.</string> <string name="icon_descr_cancel_link_preview">спри визуализацията на линка</string> @@ -140,7 +139,7 @@ <string name="callstatus_missed">пропуснато повикване</string> <string name="callstatus_rejected">отхвърлено повикване</string> <string name="callstate_starting">стартиране…</string> - <string name="onboarding_notifications_mode_periodic_desc"><b>Добър за батерията</b>. Фоновата услуга проверява съобщенията на всеки 10 минути. Може да пропуснете обаждания или спешни съобщения.</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Добър за батерията</b>. Фоновата услуга проверява съобщенията на всеки 10 минути. Може да пропуснете обаждания или спешни съобщения.]]></string> <string name="callstate_connected">свързан</string> <string name="callstate_connecting">свързване…</string> <string name="encrypted_audio_call">e2e криптиран аудио разговор</string> @@ -178,7 +177,7 @@ <string name="app_passcode_replaced_with_self_destruct">Кода за достъп до приложение се заменя с код за самоунищожение.</string> <string name="auto_accept_images">Автоматично приемане на изображения</string> <string name="authentication_cancelled">Идентификацията е отменена</string> - <string name="send_link_previews">Изпрати визуализация на линка</string> + <string name="send_link_previews">Изпрати визуализация на линковете</string> <string name="settings_section_title_calls">ОБАЖДАНИЯ</string> <string name="keychain_allows_to_receive_ntfs">Android Keystore ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на известия.</string> <string name="change_database_passphrase_question">Промяна на паролата на базата данни\?</string> @@ -187,7 +186,6 @@ <string name="rcv_conn_event_switch_queue_phase_completed">променен е адреса за вас</string> <string name="change_verb">Промени</string> <string name="change_member_role_question">Промяна на груповата роля\?</string> - <string name="incognito_random_profile_description">На вашия контакт ще бъде изпратен произволен профил</string> <string name="you_will_still_receive_calls_and_ntfs">Все още ще получавате обаждания и известия от заглушени профили, когато са активни.</string> <string name="cant_delete_user_profile">Потребителският профил не може да се изтрие!</string> <string name="allow_disappearing_messages_only_if">Позволи изчезващи съобщения само ако вашият контакт ги разрешава.</string> @@ -199,7 +197,7 @@ \nДостъпно в v5.1"</string> <string name="color_background">Фон</string> <string name="allow_message_reactions">Позволи реакции на съобщения.</string> - <string name="allow_direct_messages">Позволи изпращането на директни съобщения до членовете.</string> + <string name="allow_direct_messages">Позволи изпращането на лични съобщения до членовете.</string> <string name="allow_to_delete_messages">Позволи необратимо изтриване на изпратените съобщения.</string> <string name="allow_to_send_files">Позволи изпращане на файлове и медия.</string> <string name="allow_to_send_voice">Позволи изпращане на гласови съобщения.</string> @@ -285,7 +283,7 @@ <string name="chat_with_developers">Чат с разработчиците</string> <string name="contact_connection_pending">свързване…</string> <string name="group_connection_pending">свързване…</string> - <string name="icon_descr_server_status_connected">Свързан</string> + <string name="icon_descr_server_status_connected">Свързан със сървъра</string> <string name="confirm_verb">Потвърди</string> <string name="switch_receiving_address_question">Промени адреса за получаване\?</string> <string name="clear_verb">Изчисти</string> @@ -298,7 +296,7 @@ <string name="smp_servers_check_address">Проверете адреса на сървъра и опитайте отново.</string> <string name="clear_verification">Изчисти проверката</string> <string name="chat_lock">SimpleX заключване</string> - <string name="chat_console">Чат конзола</string> + <string name="chat_console">Конзола</string> <string name="network_session_mode_user">Чат профил</string> <string name="configure_ICE_servers">Конфигурирай ICE сървъри</string> <string name="network_session_mode_entity">Връзка</string> @@ -329,14 +327,14 @@ <string name="share_text_database_id">ID в базата данни: %d</string> <string name="receipts_section_contacts">Контакти</string> <string name="settings_section_title_themes">ТЕМИ</string> - <string name="set_password_to_export_desc">Базата данни е криптирана с произволна парола. Моля, променете я преди експортиране.</string> + <string name="set_password_to_export_desc">Базата данни е криптирана с автоматично генерирана парола. Моля, променете я преди експортиране.</string> <string name="database_passphrase">Парола за базата данни</string> <string name="delete_database">Изтрий базата данни</string> <string name="delete_chat_profile_question">Изтриване на чат профила\?</string> <string name="delete_files_and_media_question">Изтрий файлове и медия\?</string> <string name="current_passphrase">Текуща парола…</string> <string name="database_encrypted">Базата данни е криптирана!</string> - <string name="encrypted_with_random_passphrase">Базата данни е криптирана с произволна парола, можете да я промените.</string> + <string name="encrypted_with_random_passphrase">Базата данни е криптирана с автоматично генерирана парола, можете да я промените.</string> <string name="database_encryption_will_be_updated">Паролата за крптиране на базата данни ще бъде актуализирана и съхранена в Keystore.</string> <string name="database_will_be_encrypted_and_passphrase_stored">Базата данни ще бъде криптирана и паролата ще бъде съхранена в Keystore.</string> <string name="database_passphrase_will_be_updated">Паролата за криптиране на базата данни ще бъде актуализирана.</string> @@ -354,7 +352,7 @@ <string name="group_member_status_creator">създател</string> <string name="create_group_link">Създай групов линк</string> <string name="button_create_group_link">Създай линк</string> - <string name="num_contacts_selected">%d избран(и) контакт(а).</string> + <string name="num_contacts_selected">%d избран(и) контакт(а)</string> <string name="delete_group_question">Изтрий група\?</string> <string name="delete_link">Изтрий линк</string> <string name="delete_link_question">Изтрий линк\?</string> @@ -377,7 +375,7 @@ <string name="ttl_day">%d ден</string> <string name="ttl_days">%d дни</string> <string name="delete_after">Изтрий след</string> - <string name="ttl_d">%dd</string> + <string name="ttl_d">%dд</string> <string name="v5_1_custom_themes">Персонализирани теми</string> <string name="connect_via_contact_link">Свързване чрез линк на контакта\?</string> <string name="connect_via_group_link">Свързване чрез групов линк\?</string> @@ -439,4 +437,967 @@ <string name="custom_time_picker_custom">персонализиран</string> <string name="v5_1_custom_themes_descr">Персонализирайте и споделяйте цветови теми.</string> <string name="custom_time_unit_days">дни</string> + <string name="choose_file_title">Избери файл</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Ако сте получили линк за покана за SimpleX Chat, можете да го отворите във вашия браузър:</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 настолен компютър: сканирайте показания QR код от приложението чрез <b>Сканирай QR код</b>.]]></string> + <string name="delete_pending_connection__question">Изтрий предстоящата връзка\?</string> + <string name="icon_descr_email">Електронна поща</string> + <string name="share_invitation_link">Сподели еднократен линк</string> + <string name="one_time_link">Линк за еднократна покана</string> + <string name="to_verify_compare">За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства.</string> + <string name="smp_servers_scan_qr">Сканирай QR кода на сървъра</string> + <string name="smp_servers_enter_manually">Въведи сървъра ръчно</string> + <string name="smp_servers_delete_server">Изтрий сървър</string> + <string name="dont_create_address">Не създавай адрес</string> + <string name="display_name__field">Показвано име:</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с <b>двуслойно криптиране от край до край</b>.]]></string> + <string name="receipts_contacts_title_disable">Деактивирай потвърждениeто\?</string> + <string name="receipts_contacts_enable_keep_overrides">Активиране (запазване на промените)</string> + <string name="receipts_contacts_title_enable">Активирай потвърждениeто\?</string> + <string name="receipts_contacts_override_disabled">Изпращането на потвърждениe за доставка е деактивирано за %d контакта</string> + <string name="receipts_contacts_override_enabled">Изпращането на потвърждениe е активирано за %d контакта</string> + <string name="settings_section_title_device">УСТРОЙСТВО</string> + <string name="receipts_contacts_disable_keep_overrides">Деактивиране (запазване на промените)</string> + <string name="total_files_count_and_size">%d файл(а) с общ размер от %s</string> + <string name="encrypt_database">Криптирай</string> + <string name="enter_passphrase">Въведи парола…</string> + <string name="upgrade_and_open_chat">Актуализирай и отвори чата</string> + <string name="downgrade_and_open_chat">Понижи версията и отвори чата</string> + <string name="mtr_error_different">различна миграция в приложението/базата данни: %s / %s</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Вие се присъединихте към тази група. Свързване с поканващия член на групата.</string> + <string name="info_row_disappears_at">Изчезва в</string> + <string name="enter_welcome_message">Въведи съобщение при посрещане…</string> + <string name="timed_messages">Изчезващи съобщения</string> + <string name="feature_enabled">активирано</string> + <string name="feature_enabled_for_contact">активирано за контакт</string> + <string name="direct_messages_are_prohibited_in_chat">Личните съобщения между членовете са забранени в тази група.</string> + <string name="v4_5_multiple_chat_profiles_descr">Различни имена, аватари и транспортна изолация.</string> + <string name="v5_2_fix_encryption_descr">Оправяне на криптирането след възстановяване от резервни копия.</string> + <string name="delivery_receipts_title">Потвърждениe за доставка!</string> + <string name="dont_enable_receipts">Не активирай</string> + <string name="delivery_receipts_are_disabled">Потвърждениeто за доставка е деактивирано!</string> + <string name="scan_qr_to_connect_to_contact">За да се свърже, вашият контакт може да сканира QR код или да използва линка в приложението.</string> + <string name="direct_messages">Лични съобщения</string> + <string name="display_name">Показвано Име</string> + <string name="display_name_cannot_contain_whitespace">Показваното име не може да съдържа интервал.</string> + <string name="sending_delivery_receipts_will_be_enabled">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти.</string> + <string name="receipts_contacts_enable_for_all">Активиране за всички</string> + <string name="error_enabling_delivery_receipts">Грешка при активирането на потвърждениeто за доставка!</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</string> + <string name="send_receipts">Изпращане на потвърждениe за доставка</string> + <string name="you_can_enable_delivery_receipts_later">Можете да активирате по-късно през Настройки</string> + <string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.</string> + <string name="database_downgrade_warning">Предупреждение: Може да загубите някои данни!</string> + <string name="enter_correct_passphrase">Въведи правилна парола.</string> + <string name="feature_enabled_for_you">активирано за вас</string> + <string name="enter_password_to_show">Въведи парола в търсенето</string> + <string name="receipts_section_description">Тези настройки са за текущия ви профил</string> + <string name="receipts_section_description_1">Те могат да бъдат променени в настройките за всеки контакт и група.</string> + <string name="settings_developer_tools">Инструменти за разработчици</string> + <string name="receipts_contacts_disable_for_all">Деактивиране за всички</string> + <string name="settings_section_title_delivery_receipts">ИЗПРАЩАЙТЕ ПОТВЪРЖДЕНИE ЗА ДОСТАВКА НА</string> + <string name="delete_messages_after">Изтрий съобщенията след</string> + <string name="chat_item_ttl_seconds">%s секунда(и)</string> + <string name="delete_messages">Изтрий съобщенията</string> + <string name="error_encrypting_database">Грешка при криптиране на базата данни</string> + <string name="enable_automatic_deletion_question">Активиране на автоматично изтриване на съобщения\?</string> + <string name="database_is_not_encrypted">Вашата чат база данни не е криптирана - задайте парола, за да я защитите.</string> + <string name="encrypt_database_question">Криптиране на база данни\?</string> + <string name="group_invitation_item_description">покана за група %1$s</string> + <string name="you_sent_group_invitation">Изпратихте покана за групата</string> + <string name="alert_message_group_invitation_expired">Груповата покана вече е невалидна, премахната е от подателя.</string> + <string name="alert_title_group_invitation_expired">Поканата е изтекла!</string> + <string name="group_invitation_tap_to_join">Докосни за вход</string> + <string name="group_invitation_tap_to_join_incognito">Докосни за инкогнито вход</string> + <string name="group_invitation_expired">Груповата покана е изтекла</string> + <string name="you_rejected_group_invitation">Отхвърлихте поканата за групата</string> + <string name="conn_event_ratchet_sync_allowed">разрешено повторно договаряне на криптиране</string> + <string name="conn_event_ratchet_sync_required">необходимо е повторно договаряне на криптиране</string> + <string name="conn_event_ratchet_sync_agreed">криптирането е съгласувано</string> + <string name="conn_event_ratchet_sync_ok">криптирането работи</string> + <string name="snd_conn_event_ratchet_sync_ok">криптирането работи за %s</string> + <string name="snd_conn_event_ratchet_sync_allowed">разрешено повторно договаряне на криптиране за %s</string> + <string name="snd_conn_event_ratchet_sync_required">необходимо е повторно договаряне на криптиране за %s</string> + <string name="snd_conn_event_ratchet_sync_agreed">криптирането е съгласувано за %s</string> + <string name="button_edit_group_profile">Редактирай групов профил</string> + <string name="share_text_disappears_at">Изчезва в: %s</string> + <string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на \"%s\". Членът ще получи нова покана.</string> + <string name="conn_level_desc_direct">директна</string> + <string name="renegotiate_encryption">Предоговори криптирането</string> + <string name="group_display_name_field">Групово показвано име:</string> + <string name="delete_profile">Изтрий профил</string> + <string name="dont_show_again">Не показвай отново</string> + <string name="ttl_h">%dч</string> + <string name="ttl_hour">%d час</string> + <string name="ttl_hours">%d часа</string> + <string name="ttl_sec">%d сек.</string> + <string name="ttl_m">%dм</string> + <string name="ttl_min">%d мин.</string> + <string name="ttl_month">%d месец</string> + <string name="ttl_months">%d месеца</string> + <string name="ttl_mth">%dмесц.</string> + <string name="ttl_s">%dс</string> + <string name="ttl_w">%dсед.</string> + <string name="ttl_week">%d седмица</string> + <string name="ttl_weeks">%d седмици</string> + <string name="v5_2_message_delivery_receipts">Потвърждениe за доставка на съобщения!</string> + <string name="v5_2_message_delivery_receipts_descr">Втората отметка, която пропуснахме! ✅</string> + <string name="v5_2_fix_encryption">Запазете връзките си</string> + <string name="enable_receipts_all">Активирай</string> + <string name="simplex_link_mode_description">Описание</string> + <string name="simplex_link_invitation">Еднократна покана за SimpleX</string> + <string name="display_name_invited_to_connect">поканен да се свърже</string> + <string name="smp_server_test_delete_queue">Изтрий опашка</string> + <string name="smp_server_test_disconnect">Прекъсни връзката</string> + <string name="smp_server_test_download_file">Свали файл</string> + <string name="failed_to_create_user_duplicate_title">Дублирано показвано име!</string> + <string name="failed_to_create_user_duplicate_desc">Вече имате чат профил със същото показвано име. Моля, изберете друго име.</string> + <string name="la_minutes">%d минути</string> + <string name="la_seconds">%d секунди</string> + <string name="edit_verb">Редактирай</string> + <string name="delete_member_message__question">Изтрий съобщението на члена\?</string> + <string name="delete_message__question">Изтрий съобщението\?</string> + <string name="icon_descr_edited">редактиран</string> + <string name="icon_descr_server_status_disconnected">Връзката със сървъра е прекъсната</string> + <string name="scan_QR_code">Сканирай QR код</string> + <string name="disappearing_message">Изчезващо съобщение</string> + <string name="add_contact">Линк за еднократна покана</string> + <string name="sync_connection_force_question">Предоговори криптирането\?</string> + <string name="sync_connection_force_desc">Криптирането работи и новото споразумение за криптиране не е необходимо. Това може да доведе до грешки при свързване!</string> + <string name="edit_image">Редактирай изображение</string> + <string name="status_e2e_encrypted">e2e криптиран</string> + <string name="status_no_e2e_encryption">липсва e2e криптиране</string> + <string name="integrity_msg_duplicate">дублирано съобщение</string> + <string name="privacy_and_security">Поверителност и сигурност</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Това може да се случи, когато вие или вашата връзка използвате старо резервно копие на базата данни.</string> + <string name="self_destruct_new_display_name">Ново показвано име:</string> + <string name="enable_lock">Активирай заключване</string> + <string name="enable_self_destruct">Активирай самоунищожение</string> + <string name="chat_item_ttl_none">никога</string> + <string name="encrypted_database">Криптирана база данни</string> + <string name="network_option_enable_tcp_keep_alive">Активирай TCP keep-alive</string> + <string name="disappearing_prohibited_in_this_chat">Изчезващите съобщения са забранени в този чат.</string> + <string name="disappearing_messages_are_prohibited">Изчезващите съобщения са забранени в тази група.</string> + <string name="v4_4_disappearing_messages">Изчезващи съобщения</string> + <string name="enter_welcome_message_optional">Въведи съобщение при посрещане…(незадължително)</string> + <string name="button_welcome_message">Съобщение при посрещане</string> + <string name="full_name_optional__prompt">Пълно име (незадължително)</string> + <string name="icon_descr_server_status_error">Грешка при свързване със сървъра</string> + <string name="v4_2_auto_accept_contact_requests_desc">С незадължително съобщение при посрещане.</string> + <string name="error_deleting_database">Грешка при изтриване на чат базата данни</string> + <string name="error_changing_message_deletion">Грешка при промяна на настройката</string> + <string name="error_creating_link_for_group">Грешка при създаване на групов линк</string> + <string name="error_deleting_link_for_group">Грешка при изтриване на групов линк</string> + <string name="error_changing_role">Грешка при промяна на ролята</string> + <string name="save_welcome_message_question">Запази съобщението при посрещане\?</string> + <string name="group_welcome_title">Съобщение при посрещане</string> + <string name="v4_6_group_welcome_message_descr">Задай съобщението, показано на новите членове!</string> + <string name="v4_6_group_welcome_message">Съобщение при посрещане в групата</string> + <string name="server_error">грешка</string> + <string name="failed_to_create_user_title">Грешка при създаване на профил!</string> + <string name="error_aborting_address_change">Грешка при отказване на промяна на адреса</string> + <string name="error_accepting_contact_request">Грешка при приемане на заявка за контакт</string> + <string name="error_adding_members">Грешка при добавяне на член(ове)</string> + <string name="error_changing_address">Грешка при промяна на адреса</string> + <string name="error_creating_address">Грешка при създаване на адрес</string> + <string name="error_deleting_contact">Грешка при изтриване на контакт</string> + <string name="error_deleting_contact_request">Грешка при изтриване на заявка за контакт</string> + <string name="error_deleting_group">Грешка при изтриване на група</string> + <string name="error_deleting_pending_contact_connection">Грешка при изтриване на предстоящата контактна връзка</string> + <string name="error_saving_ICE_servers">Грешка при запазване на ICE сървърите</string> + <string name="icon_descr_server_status_pending">Предстояща връзка със сървъра</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Вашият контакт трябва да бъде онлайн, за да осъществите връзката. +\nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк).</string> + <string name="error_exporting_chat_database">Грешка при експортиране на чат базата данни</string> + <string name="error_with_info">Грешка: %s</string> + <string name="error_saving_file">Грешка при запазване на файл</string> + <string name="error_importing_database">Грешка при импортиране на чат базата данни</string> + <string name="error_removing_member">Грешка при отстраняване на член</string> + <string name="error_saving_group_profile">Грешка при запазване на профила на групата</string> + <string name="error_loading_smp_servers">Грешка при зареждане на SMP сървъри</string> + <string name="error_loading_xftp_servers">Грешка при зареждане на XFTP сървъри</string> + <string name="error_saving_smp_servers">Грешка при запазване на SMP сървърите</string> + <string name="error_joining_group">Грешка при присъединяване към група</string> + <string name="error_loading_details">Грешка при зареждане на подробности</string> + <string name="error_receiving_file">Грешка при получаване на файл</string> + <string name="error_deleting_user">Грешка при изтриване на потребителския профил</string> + <string name="error_saving_user_password">Грешка при запазване на потребителска парола</string> + <string name="error_starting_chat">Грешка при стартиране на чата</string> + <string name="error_stopping_chat">Грешка при спиране на чата</string> + <string name="error_saving_xftp_servers">Грешка при запазване на XFTP сървърите</string> + <string name="error_sending_message">Грешка при изпращане на съобщение</string> + <string name="error_setting_address">Грешка при задаване на адрес</string> + <string name="failed_to_active_user_title">Грешка при смяна на профил!</string> + <string name="error_synchronizing_connection">Грешка при синхронизиране на връзката</string> + <string name="favorite_chat">Любим</string> + <string name="full_name__field">Пълно име:</string> + <string name="exit_without_saving">Изход без запазване</string> + <string name="hidden_profile_password">Парола за скрит профил</string> + <string name="settings_section_title_experimenta">ЕКСПЕРИМЕНТАЛЕН</string> + <string name="file_with_path">Файл: %s</string> + <string name="icon_descr_expand_role">Разшири избора на роля</string> + <string name="fix_connection_question">Поправи връзката\?</string> + <string name="fix_connection_not_supported_by_contact">Поправката не се поддържа от контакта</string> + <string name="fix_connection">Поправи връзката</string> + <string name="fix_connection_confirm">Поправи</string> + <string name="v4_4_disappearing_messages_desc">Изпратените съобщения ще бъдат изтрити след зададеното време.</string> + <string name="group_link">Групов линк</string> + <string name="files_and_media">Файлове и медия</string> + <string name="section_title_for_console">ЗА КОНЗОЛАТА</string> + <string name="group_preferences">Групови настройки</string> + <string name="icon_descr_file">Файл</string> + <string name="file_not_found">Файлът не е намерен</string> + <string name="files_and_media_prohibited">Файловете и медията са забранени!</string> + <string name="file_saved">Файлът е запазен</string> + <string name="file_will_be_received_when_contact_completes_uploading">Файлът ще бъде получен, когато вашият контакт завърши качването му.</string> + <string name="file_will_be_received_when_contact_is_online">Файлът ще бъде получен, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</string> + <string name="v5_2_favourites_filter_descr">Филтрирайте непрочетените и любимите чатове.</string> + <string name="group_members_can_send_dms">Членовете на групата могат да изпращат лични съобщения.</string> + <string name="icon_descr_help">помощ</string> + <string name="settings_section_title_help">ПОМОЩ</string> + <string name="email_invite_body">Здравей, +\nСвържи се с мен през SimpleX Chat: %s</string> + <string name="group_members_can_add_message_reactions">Членовете на групата могат да добавят реакции към съобщенията.</string> + <string name="group_members_can_delete">Членовете на групата могат необратимо да изтриват изпратените съобщения.</string> + <string name="group_members_can_send_voice">Членовете на групата могат да изпращат гласови съобщения.</string> + <string name="v5_2_disappear_one_message_descr">Дори когато е деактивиран в разговора.</string> + <string name="v5_0_large_files_support_descr">Бързо и без чакане, докато подателят е онлайн!</string> + <string name="v4_4_french_interface">Френски интерфейс</string> + <string name="v4_2_group_links">Групови линкове</string> + <string name="settings_experimental_features">Експериментални функции</string> + <string name="export_database">Експортирай база данни</string> + <string name="icon_descr_group_inactive">Групата е неактивна</string> + <string name="alert_title_no_group">Групата не е намерена!</string> + <string name="snd_group_event_group_profile_updated">профилът на групата е актуализиран</string> + <string name="group_member_status_group_deleted">групата е изтрита</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Групата ще бъде изтрита за всички членове - това не може да бъде отменено!</string> + <string name="delete_group_for_self_cannot_undo_warning">Групата ще бъде изтрита за вас - това не може да бъде отменено!</string> + <string name="error_updating_link_for_group">Грешка при актуализиране на груповия линк</string> + <string name="info_row_group">Група</string> + <string name="fix_connection_not_supported_by_group_member">Поправката не се поддържа от члена на групата</string> + <string name="group_full_name_field">Пълно име на групата:</string> + <string name="group_profile_is_stored_on_members_devices">Груповият профил се съхранява на устройствата на членовете, а не на сървърите.</string> + <string name="user_hide">Скрий</string> + <string name="prohibit_sending_disappearing_messages">Забрани изпращането на изчезващи съобщения.</string> + <string name="files_are_prohibited_in_group">Файловете и медията са забранени в тази група.</string> + <string name="group_members_can_send_files">Членовете на групата могат да изпращат файлове и медия.</string> + <string name="v4_6_hidden_chat_profiles">Скрити чат профили</string> + <string name="v4_6_reduced_battery_usage">Допълнително намален разход на батерията</string> + <string name="v4_6_group_moderation">Групово модериране</string> + <string name="v5_1_message_reactions_descr">Най-накрая ги имаме! 🚀</string> + <string name="v5_2_favourites_filter">Намирайте чатове по-бързо</string> + <string name="error_setting_network_config">Грешка при актуализиране на мрежовата конфигурация</string> + <string name="failed_to_parse_chats_title">Неуспешно зареждане на чатовете</string> + <string name="failed_to_parse_chat_title">Неуспешно зареждане на чата</string> + <string name="simplex_link_mode_full">Цял линк</string> + <string name="error_updating_user_privacy">Грешка при актуализиране на поверителността на потребителя</string> + <string name="hide_notification">Скрий</string> + <string name="revoke_file__message">Файлът ще бъде изтрит от сървърите.</string> + <string name="for_everybody">За всички</string> + <string name="hide_verb">Скрий</string> + <string name="choose_file">Файл</string> + <string name="from_gallery_button">От Галерия</string> + <string name="hide_dev_options">Скрий:</string> + <string name="icon_descr_flip_camera">Обърни камерата</string> + <string name="icon_descr_hang_up">Затвори</string> + <string name="files_and_media_section">Файлове и медия</string> + <string name="only_you_can_send_disappearing">Само вие можете да изпращате изчезващи съобщения.</string> + <string name="only_your_contact_can_send_disappearing">Само вашият контакт може да изпраща изчезващи съобщения.</string> + <string name="prohibit_sending_disappearing">Забрани изпращането на изчезващи съобщения.</string> + <string name="group_members_can_send_disappearing">Членовете на групата могат да изпращат изчезващи съобщения.</string> + <string name="invalid_QR_code">Невалиден QR код</string> + <string name="invalid_contact_link">Невалиден линк!</string> + <string name="incorrect_code">Неправилен код за сигурност!</string> + <string name="smp_servers_invalid_address">Невалиден адрес на сървъра!</string> + <string name="how_to_use_your_servers">Как да използвате вашите сървъри</string> + <string name="enter_one_ICE_server_per_line">ICE сървъри (по един на ред)</string> + <string name="host_verb">Хост</string> + <string name="network_disable_socks_info">Ако потвърдите, сървърите за съобщения ще могат да виждат вашия IP адрес, а вашият интернет доставчик - към кои сървъри се свързвате.</string> + <string name="invite_friends">Покани приятели</string> + <string name="hide_profile">Скрий профила</string> + <string name="how_to_use_markdown">Как се използва форматирането</string> + <string name="onboarding_notifications_mode_subtitle">Може да се промени по-късно през настройките.</string> + <string name="onboarding_notifications_mode_service">Незабавно</string> + <string name="settings_section_title_incognito">Режим инкогнито</string> + <string name="icon_descr_add_members">Покани членове</string> + <string name="initial_member_role">Първоначална роля</string> + <string name="invite_to_group_button">Покани в групата</string> + <string name="v4_3_irreversible_message_deletion">Необратимо изтриване на съобщение</string> + <string name="unhide_chat_profile">Покажи чат профила</string> + <string name="unhide_profile">Покажи профила</string> + <string name="how_to_use_simplex_chat">Как се използва</string> + <string name="italic_text">курсив</string> + <string name="import_database_question">Импортиране на чат база данни\?</string> + <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Ако изберете да отхвърлите, подателят НЯМА да бъде уведомен.</string> + <string name="image_descr">Изображение</string> + <string name="image_will_be_received_when_contact_completes_uploading">Изображението ще бъде получено, когато вашият контакт завърши качването му.</string> + <string name="image_saved">Изображението е запазено в Галерия</string> + <string name="icon_descr_image_snd_complete">Изображението е изпратено</string> + <string name="image_will_be_received_when_contact_is_online">Изображението ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</string> + <string name="message_deletion_prohibited_in_chat">Необратимото изтриване на съобщения е забранено в тази група.</string> + <string name="v4_3_improved_server_configuration">Подобрена конфигурация на сървъра</string> + <string name="alert_text_skipped_messages_it_can_happen_when">Това може да се случи, когато: +\n1. Времето за пазене на съобщенията е изтекло - в изпращащия клиент е 2 дена а на сървъра е 30. +\n2. Декриптирането на съобщението е неуспешно, защото вие или вашият контакт сте използвали старо копие на базата данни. +\n3. Връзката е била компрометирана.</string> + <string name="import_database">Импортиране на база данни</string> + <string name="import_database_confirmation">Импортиране</string> + <string name="incompatible_database_version">Несъвместима версия на базата данни</string> + <string name="invalid_migration_confirmation">Невалидно потвърждение за мигриране</string> + <string name="join_group_button">Присъединяване</string> + <string name="rcv_group_event_member_added">покани %1$s</string> + <string name="rcv_group_event_invited_via_your_group_link">поканен чрез вашия групов линк</string> + <string name="group_member_status_invited">поканен</string> + <string name="conn_level_desc_indirect">индиректна (%1$s)</string> + <string name="user_unhide">Покажи</string> + <string name="you_can_hide_or_mute_user_profile">Можете да скриете или заглушите потребителски профил - задръжте върху него за менюто.</string> + <string name="incognito">Инкогнито</string> + <string name="incognito_info_protects">Режимът инкогнито защитава вашата поверителност, като използва нов автоматично генериран профил за всеки контакт.</string> + <string name="incognito_info_allows">Позволява да имате много анонимни връзки без споделени данни между тях в един чат профил .</string> + <string name="v4_5_italian_interface">Италиански интерфейс</string> + <string name="description_via_contact_address_link_incognito">инкогнито чрез линк с адрес за контакт</string> + <string name="description_via_group_link_incognito">инкогнито чрез групов линк</string> + <string name="description_via_one_time_link_incognito">инкогнито чрез еднократен линк за връзка</string> + <string name="invalid_chat">невалиден чат</string> + <string name="invalid_data">невалидни данни</string> + <string name="invalid_message_format">невалиден формат на съобщението</string> + <string name="invalid_connection_link">Невалиден линк за връзка</string> + <string name="notification_display_mode_hidden_desc">Скриване на контакт и съобщение</string> + <string name="turn_off_battery_optimization"><![CDATA[За да го използвате, моля, <b>разрешете на SimpleX да работи във фонов режим</b> в следващия диалогов прозорец. В противен случай известията ще бъдат деактивирани.]]></string> + <string name="icon_descr_instant_notifications">Незабавни известия</string> + <string name="service_notifications">Незабавни известия!</string> + <string name="service_notifications_disabled">Незабавните известия са деактивирани!</string> + <string name="edit_history">История</string> + <string name="la_immediately">Веднага</string> + <string name="info_menu">Информация</string> + <string name="in_reply_to">В отговор на</string> + <string name="gallery_image_button">Изображение</string> + <string name="how_to">Информация</string> + <string name="install_simplex_chat_for_terminal">Инсталирай SimpleX Chat за терминал</string> + <string name="how_it_works">Как работи</string> + <string name="how_simplex_works">Как работи SimpleX</string> + <string name="immune_to_spam_and_abuse">Защитен от спам и злоупотреби</string> + <string name="ignore">Игнорирай</string> + <string name="button_add_members">Покани членове</string> + <string name="message_deletion_prohibited">Необратимото изтриване на съобщения е забранено в този чат.</string> + <string name="v4_3_improved_privacy_and_security_desc">Скриване на екрана на приложението в изгледа на скоро отворнените приложения.</string> + <string name="v4_3_improved_privacy_and_security">Подобрена поверителност и сигурност</string> + <string name="custom_time_unit_hours">часове</string> + <string name="custom_time_unit_minutes">минути</string> + <string name="custom_time_unit_months">месеци</string> + <string name="custom_time_unit_seconds">секунди</string> + <string name="custom_time_unit_weeks">седмици</string> + <string name="v5_1_japanese_portuguese_interface">Японски и португалски потребителски интерфейс</string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобилно: докоснете <b>Отваряне в мобилно приложение</b>, след което докоснете <b>Свързване</b> в приложението.]]></string> + <string name="reject_contact_button">Отхвърляне</string> + <string name="mark_unread">Маркирай като непрочетено</string> + <string name="mark_read">Маркирай като прочетено</string> + <string name="mute_chat">Без звук</string> + <string name="image_descr_qr_code">QR код</string> + <string name="icon_descr_more_button">Повече</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Прочетете повече в <font color=#0088ff>Ръководство за потребителя</font>.]]></string> + <string name="mark_code_verified">Маркирай като проверено</string> + <string name="is_not_verified">%s не е потвърдено</string> + <string name="is_verified">%s е потвърдено</string> + <string name="rate_the_app">Оценете приложението</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Уверете се, че адресите на WebRTC ICE сървъра са в правилен формат, разделени на редове и не са дублирани.</string> + <string name="network_and_servers">Мрежа и сървъри</string> + <string name="network_settings_title">Мрежови настройки</string> + <string name="port_verb">Порт</string> + <string name="network_proxy_port">порт %d</string> + <string name="network_use_onion_hosts_no_desc_in_alert">Няма се използват Onion хостове.</string> + <string name="network_use_onion_hosts_required">Задължително</string> + <string name="network_use_onion_hosts_no">Не</string> + <string name="network_use_onion_hosts_required_desc">За свързване ще са необходими Onion хостове. +\nМоля, обърнете внимание: няма да можете да се свържете със сървърите без .onion адрес.</string> + <string name="network_use_onion_hosts_prefer_desc">Ще се използват Onion хостове, когато са налични.</string> + <string name="network_use_onion_hosts_prefer_desc_in_alert">Ще се използват Onion хостове, когато са налични.</string> + <string name="network_use_onion_hosts_no_desc">Няма се използват Onion хостове.</string> + <string name="email_invite_subject">Нека да поговорим в SimpleX Chat</string> + <string name="password_to_show">Парола за показване</string> + <string name="no_spaces">Без интервали!</string> + <string name="read_more_in_github_with_link"><![CDATA[Прочетете повече в нашето <font color=#0088ff>GitHub хранилище</font>.]]></string> + <string name="onboarding_notifications_mode_off">Когато приложението работи</string> + <string name="onboarding_notifications_mode_periodic">Периодично</string> + <string name="paste_the_link_you_received">Постави получения линк</string> + <string name="settings_section_title_messages">СЪОБЩЕНИЯ И ФАЙЛОВЕ</string> + <string name="no_received_app_files">Няма получени или изпратени файлове</string> + <string name="notifications_will_be_hidden">Известията ще се доставят само докато приложението не е спряно!</string> + <string name="remove_passphrase_from_keychain">Премахване на парола от Keystore\?</string> + <string name="new_passphrase">Нова парола…</string> + <string name="remove_passphrase">Премахване</string> + <string name="database_migrations">Миграции: %s</string> + <string name="joining_group">Присъединяване към групата</string> + <string name="leave_group_button">Напусни</string> + <string name="leave_group_question">Напусни групата\?</string> + <string name="info_row_moderated_at">Модерирано в</string> + <string name="share_text_updated_at">Записът е актуализиран на: %s</string> + <string name="share_text_received_at">Получено в: %s</string> + <string name="share_text_moderated_at">Модерирано в: %s</string> + <string name="color_surface">Менюта и сигнали</string> + <string name="color_received_message">Получено съобщение</string> + <string name="feature_off">изключено</string> + <string name="v4_3_voice_messages_desc">Макс. 40 секунди, получават се незабавно.</string> + <string name="v4_4_live_messages">Съобщения на живо</string> + <string name="live_message">Съобщение на живо!</string> + <string name="verify_security_code">Потвръди кода за сигурност</string> + <string name="no_details">няма подробности</string> + <string name="ok">ОК</string> + <string name="ask_your_contact_to_enable_voice">Моля, попитайте вашия контакт, за да активирате изпращане на гласови съобщения.</string> + <string name="icon_descr_record_voice_message">Запис на гласово съобщение</string> + <string name="sync_connection_force_confirm">Предоговоряне</string> + <string name="join_group_incognito_button">Влез инкогнито</string> + <string name="info_row_local_name">Локално име</string> + <string name="message_reactions">Реакции на съобщения</string> + <string name="network_status">Състояние на мрежата</string> + <string name="network_option_ping_interval">PING интервал</string> + <string name="info_row_received_at">Получено в</string> + <string name="feature_received_prohibited">получено, забранено</string> + <string name="receiving_via">Получаване чрез</string> + <string name="info_row_updated_at">Записът е актуализиран на</string> + <string name="learn_more">Научете повече</string> + <string name="paste_button">Постави</string> + <string name="smp_servers_preset_server">Предварително зададен сървър</string> + <string name="smp_servers_preset_address">Предварително зададен адрес на сървъра</string> + <string name="onboarding_notifications_mode_title">Поверителени известия</string> + <string name="la_mode_off">Изключено</string> + <string name="reject">Отхвърляне</string> + <string name="prohibit_sending_voice">Забрани изпращането на гласови съобщения.</string> + <string name="prohibit_message_reactions_group">Забрани реакциите на съобщенията.</string> + <string name="restore_database_alert_desc">Моля, въведете предишната парола след възстановяване на резервното копие на базата данни. Това действие не може да бъде отменено.</string> + <string name="large_file">Голям файл!</string> + <string name="message_reactions_are_prohibited">Реакциите на съобщения са забранени в тази група.</string> + <string name="new_in_version">Ново в %s</string> + <string name="feature_offered_item">предлага %s</string> + <string name="feature_offered_item_with_param">предлага %s: %2s</string> + <string name="whats_new_read_more">Прочетете още</string> + <string name="v5_2_disappear_one_message">Накарайте едно съобщение да изчезне</string> + <string name="v4_4_verify_connection_security">Потвръди сигурността на връзката</string> + <string name="v4_5_message_draft">Чернова на съобщение</string> + <string name="v5_2_more_things_descr">- по-стабилна доставка на съобщения. +\n- малко по-добри групи. +\n- и още!</string> + <string name="v4_5_multiple_chat_profiles">Множество профили за чат</string> + <string name="in_developing_title">Очаквайте скоро!</string> + <string name="markdown_help">Форматиране на съобщения помощ</string> + <string name="markdown_in_messages">Форматиране на съобщения</string> + <string name="no_selected_chat">Няма избран чат</string> + <string name="notifications">Известия</string> + <string name="shutdown_alert_desc">Известията ще спрат да работят, докато не стартирате отново приложението</string> + <string name="network_use_onion_hosts_required_desc_in_alert">За свързване ще са необходими Onion хостове.</string> + <string name="images_limit_desc">Само 10 изображения могат да бъдат изпратени едновременно</string> + <string name="only_owners_can_enable_files_and_media">Само собствениците на групата могат да активират файлове и медията.</string> + <string name="only_group_owners_can_enable_voice">Само собствениците на групата могат да активират гласови съобщения.</string> + <string name="only_stored_on_members_devices">(съхранено само на устройствата на членовете на групата)</string> + <string name="restore_passphrase_not_found_desc">Паролата не е намерена в KeyStore, моля, въведете я ръчно. Това може да се е случило, ако възстановите данните на приложението с помощта на инструмент за резервни копия. Ако не е така, моля, свържете се с разработчиците.</string> + <string name="observer_cant_send_message_desc">Моля, свържете се с груповия администартор.</string> + <string name="you_can_accept_or_reject_connection">Когато хората искат да се свържат с вас, можете да ги приемете или отхвърлите.</string> + <string name="profile_update_will_be_sent_to_contacts">Актуализацията на профила ще бъде изпратена до вашите контакти.</string> + <string name="prohibit_message_deletion">Забрани необратимото изтриване на съобщения.</string> + <string name="prohibit_direct_messages">Забрани изпращането на лични съобщения до членовете.</string> + <string name="receiving_files_not_yet_supported">получаването на файлове все още не се поддържа</string> + <string name="switch_receiving_address_desc">Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн.</string> + <string name="new_database_archive">Нов архив на база данни</string> + <string name="old_database_archive">Стар архив на база данни</string> + <string name="non_fatal_errors_occured_during_import">Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности.</string> + <string name="messages_section_title">Съобщения</string> + <string name="enter_correct_current_passphrase">Моля, въведете правилната текуща парола.</string> + <string name="store_passphrase_securely">Моля, съхранявайте паролата на сигурно място, НЯМА да можете да я промените, ако я загубите.</string> + <string name="store_passphrase_securely_without_recover">Моля, съхранявайте паролата на сигурно място, НЯМА да имате достъп до чата, ако я загубите.</string> + <string name="keychain_error">Keychain грешка</string> + <string name="open_chat">Отвори чат</string> + <string name="join_group_question">Влез в групата\?</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Поканени сте в групата. Присъединете се, за да се свържете с членове на групата.</string> + <string name="you_joined_this_group">Вие се присъединихте към тази група</string> + <string name="rcv_group_event_member_left">напусна</string> + <string name="rcv_group_event_member_deleted">отстранен %1$s</string> + <string name="rcv_group_event_user_deleted">ви острани</string> + <string name="group_member_status_left">напусна</string> + <string name="group_member_role_member">член</string> + <string name="group_member_role_observer">наблюдател</string> + <string name="group_member_role_owner">собственик</string> + <string name="rcv_conn_event_verification_code_reset">кодът за сигурност е променен</string> + <string name="group_member_status_removed">отстранен</string> + <string name="new_member_role">Нова членска роля</string> + <string name="no_contacts_to_add">Няма контакти за добавяне</string> + <string name="button_leave_group">Напусни групата</string> + <string name="no_contacts_selected">Няма избрани контакти</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Можете да споделите линк или QR код - всеки ще може да се присъедини към групата. Няма да загубите членовете на групата, ако по-късно я изтриете.</string> + <string name="only_group_owners_can_change_prefs">Само собствениците на групата могат да променят груповите настройки.</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Членът ще бъде премахнат от групата - това не може да бъде отменено!</string> + <string name="item_info_no_text">няма текст</string> + <string name="button_remove_member">Острани член</string> + <string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени.</string> + <string name="users_delete_data_only">Само данни за локален профил</string> + <string name="users_delete_with_connections">Профилни и сървърни връзки</string> + <string name="user_mute">Без звук</string> + <string name="make_profile_private">Направи профила поверителен!</string> + <string name="muted_when_inactive">Без звук при неактивност!</string> + <string name="profile_password">Профилна парола</string> + <string name="chat_preferences_no">не</string> + <string name="chat_preferences_off">изключено</string>` + <string name="chat_preferences_on">включено</string> + <string name="v4_4_live_messages_desc">Получателите виждат актуализации, докато ги въвеждате.</string> + <string name="v4_5_reduced_battery_usage_descr">Очаквайте скоро още подобрения!</string> + <string name="v4_5_message_draft_descr">Запазете последната чернова на съобщението с прикачени файлове.</string> + <string name="v4_5_private_filenames">Поверителни имена на файлове</string> + <string name="v4_5_reduced_battery_usage">Намалена консумация на батерията</string> + <string name="v4_6_hidden_chat_profiles_descr">Защитете чат профилите с парола!</string> + <string name="v4_6_group_moderation_descr">Сега администраторите могат: +\n- да изтриват съобщения на членове. +\n- да деактивират членове (роля \"наблюдател\")</string> + <string name="v4_6_reduced_battery_usage_descr">Очаквайте скоро още подобрения!</string> + <string name="v5_0_polish_interface">Полски интерфейс</string> + <string name="v5_1_message_reactions">Реакции на съобщения</string> + <string name="you_will_join_group">Ще се присъедините към групата, към която този линк препраща, и ще се свържете с нейните членове.</string> + <string name="thousand_abbreviation">х</string> + <string name="opening_database">Отваряне на база данни…</string> + <string name="live">НА ЖИВО</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Уверете се, че адресите на SMP сървъра са в правилен формат, разделени на редове и не са дублирани.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Уверете се, че адресите на XFTP сървъра са в правилен формат, разделени на редове и не са дублирани.</string> + <string name="marked_deleted_description">маркирано като изтрито</string> + <string name="moderated_description">модерирано</string> + <string name="moderated_item_description">модерирано от %s</string> + <string name="simplex_link_mode_browser_warning">Отварянето на линка в браузъра може да намали поверителността и сигурността на връзката. Несигурните SimpleX линкове ще бъдат червени.</string> + <string name="contact_developers">Моля, актуализирайте приложението и се свържете с разработчиците.</string> + <string name="network_error_desc">Моля, проверете мрежовата си връзка с %1$s и опитайте отново.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг.</string> + <string name="error_smp_test_certificate">Въжможно е пръстовият отпечатък на сертификата в адреса на сървъра да е неправилен</string> + <string name="notification_new_contact_request">Нова заявка за контакт</string> + <string name="enter_passphrase_notification_title">Необходима е парола</string> + <string name="periodic_notifications">Периодични известия</string> + <string name="periodic_notifications_disabled">Периодичните известия са деактивирани!</string> + <string name="simplex_service_notification_text">Получаване на съобщения…</string> + <string name="notifications_mode_off">Работи, когато приложението е отворено</string> + <string name="simplex_service_notification_title">Simplex Chat услуга</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[За да запази вашата поверителност, вместо да изпозлва push известия, приложението има <b> SimpleX фонова услуга </b> – използва няколко процента от батерията на ден.]]></string> + <string name="enter_passphrase_notification_desc">За да получавате известия, моля, въведете паролата на базата данни</string> + <string name="auth_log_in_using_credential">Влезте с вашите идентификационни данни</string> + <string name="message_delivery_error_title">Грешка при доставката на съобщението</string> + <string name="la_could_not_be_verified">Не можахте да бъдете потвърдени; Моля, опитайте отново.</string> + <string name="message_delivery_error_desc">Най-вероятно този контакт е изтрил връзката с вас.</string> + <string name="no_history">Няма история</string> + <string name="auth_open_chat_console">Отвори конзолата</string> + <string name="auth_open_chat_profiles">Отвори чат профилите</string> + <string name="la_please_remember_to_store_password">Моля, запомнете я или я съхранявайте на сигурно място - няма начин да възстановите загубена парола!</string> + <string name="received_message">Получено съобщение</string> + <string name="reply_verb">Отговори</string> + <string name="delete_message_cannot_be_undone_warning">Съобщението ще бъде изтрито - това не може да бъде отменено!</string> + <string name="delete_message_mark_deleted_warning">Съобщението ще бъде маркирано за изтриване. Получателят(ите) ще може(гат) да разкрие(ят) това съобщение.</string> + <string name="moderate_verb">Модерирай</string> + <string name="moderate_message_will_be_deleted_warning">Съобщението ще бъде изтрито за всички членове.</string> + <string name="moderate_message_will_be_marked_warning">Съобщението ще бъде маркирано като модерирано за всички членове.</string> + <string name="no_filtered_chats">Няма филтрирани чатове</string> + <string name="stop_rcv_file__message">Получаващият се файл ще бъде спрян.</string> + <string name="videos_limit_desc">Само 10 видеоклипа могат да бъдат изпратени едновременно</string> + <string name="toast_permission_denied">Разрешение е отказано!</string> + <string name="image_descr_profile_image">профилно изображение</string> + <string name="icon_descr_profile_image_placeholder">запазено място за профилно изображение</string> + <string name="opensource_protocol_and_code_anybody_can_run_servers">Протокол и код с отворен код – всеки може да оперира собствени сървъри.</string> + <string name="people_can_connect_only_via_links_you_share">Хората могат да се свържат с вас само чрез ликовете, които споделяте.</string> + <string name="privacy_redefined">Поверителността преосмислена</string> + <string name="read_more_in_github">Прочетете повече в нашето хранилище в GitHub.</string> + <string name="make_private_connection">Добави поверителна връзка</string> + <string name="many_people_asked_how_can_it_deliver"><![CDATA[Много хора попитаха: <i>ако SimpleX няма потребителски идентификатори, как може да доставя съобщения\?</i>]]></string> + <string name="open_verb">Отвори</string> + <string name="relay_server_if_necessary">Реле сървър се използва само ако е необходимо. Друга страна може да наблюдава вашия IP адрес.</string> + <string name="lock_after">Заключване след</string> + <string name="lock_mode">Режим на заключване</string> + <string name="alert_text_fragment_please_report_to_developers">Моля, докладвайте го на разработчиците.</string> + <string name="protect_app_screen">Защити екрана на приложението</string> + <string name="save_archive">Запази архив</string> + <string name="member_info_section_title_member">ЧЛЕН</string> + <string name="remove_member_confirmation">Премахване</string> + <string name="network_option_ping_count">PING бройка</string> + <string name="only_your_contact_can_add_message_reactions">Само вашият контакт може да добавя реакции на съобщенията.</string> + <string name="message_reactions_prohibited_in_this_chat">Реакциите на съобщения са забранени в този чат.</string> + <string name="only_you_can_add_message_reactions">Само вие можете да добавяте реакции на съобщенията.</string> + <string name="only_you_can_delete_messages">Само вие можете необратимо да изтриете съобщения (вашият контакт може да ги маркира за изтриване).</string> + <string name="only_you_can_send_voice">Само вие можете да изпращате гласови съобщения.</string> + <string name="only_your_contact_can_delete">Само вашият контакт може необратимо да изтрие съобщения (можете да ги маркирате за изтриване).</string> + <string name="only_your_contact_can_send_voice">Само вашият контакт може да изпраща гласови съобщения.</string> + <string name="prohibit_message_reactions">Забрани реакциите на съобщенията.</string> + <string name="prohibit_sending_voice_messages">Забрани изпращането на гласови съобщения.</string> + <string name="prohibit_sending_files">Забрани изпращането на файлове и медия.</string> + <string name="set_contact_name">Задай име на контакт</string> + <string name="show_QR_code">Покажи QR код</string> + <string name="scan_code">Сканирай код</string> + <string name="scan_code_from_contacts_app">Сканирайте кода за сигурност от приложението на вашия контакт.</string> + <string name="security_code">Код за сигурност</string> + <string name="chat_with_the_founder">Изпращайте въпроси и идеи</string> + <string name="smp_save_servers_question">Запази сървърите\?</string> + <string name="saved_ICE_servers_will_be_removed">Запазените WebRTC ICE сървъри ще бъдат премахнати.</string> + <string name="save_servers_button">Запази</string> + <string name="network_socks_proxy_settings">SOCKS прокси настройки</string> + <string name="show_developer_options">Покажи опциите за разработчици</string> + <string name="save_auto_accept_settings">Запази настройките за автоматично приемане</string> + <string name="save_settings_question">Запази настройките\?</string> + <string name="save_profile_password">Запази паролата на профила</string> + <string name="stop_chat_question">Спри чата\?</string> + <string name="set_password_to_export">Задай парола за експортиране</string> + <string name="stop_chat_confirmation">Спри</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Спрете чата, за да експортирате, импортирате или изтриете чат базата данни. Няма да можете да получавате и изпращате съобщения, докато чатът е спрян.</string> + <string name="save_passphrase_in_keychain">Запази паролата в Keystore</string> + <string name="save_passphrase_and_open_chat">Запази паролата и отвори чата</string> + <string name="restore_database_alert_confirm">Възстанови</string> + <string name="database_restore_error">Грешка при възстановяване на базата данни</string> + <string name="select_contacts">Избери контакти</string> + <string name="share_address">Сподели адрес</string> + <string name="share_text_sent_at">Изпратено на: %s</string> + <string name="network_options_reset_to_defaults">Възстановяване на настройките по подразбиране</string> + <string name="network_option_seconds_label">сек.</string> + <string name="send_disappearing_message_send">Изпрати</string> + <string name="send_disappearing_message">Изпрати изчезващо съобщение</string> + <string name="send_live_message">Изпрати съобщение на живо</string> + <string name="icon_descr_send_message">Изпрати съобщение</string> + <string name="skip_inviting_button">Пропусни покана на членове</string> + <string name="info_row_sent_at">Изпратено на</string> + <string name="icon_descr_address">SimpleX Адрес</string> + <string name="image_descr_simplex_logo">SimpleX Лого</string> + <string name="icon_descr_simplex_team">SimpleX Екип</string> + <string name="smp_servers">SMP сървъри</string> + <string name="share_address_with_contacts_question">Сподели адреса с контактите\?</string> + <string name="share_link">Сподели линк</string> + <string name="share_with_contacts">Сподели с контактите</string> + <string name="stop_sharing">Спри споделянето</string> + <string name="stop_sharing_address">Спри споделянето на адреса\?</string> + <string name="settings_section_title_settings">НАСТРОЙКИ</string> + <string name="run_chat_section">СТАРТИРАНЕ НА ЧАТ</string> + <string name="text_field_set_contact_placeholder">Задай име на контакт…</string> + <string name="no_info_on_delivery">Няма информация за доставката</string> + <string name="revoke_file__title">Отзови файл\?</string> + <string name="stop_file__confirm">Спри</string> + <string name="stop_rcv_file__title">Спри получаването на файла\?</string> + <string name="stop_snd_file__title">Спри изпращането на файла\?</string> + <string name="icon_descr_sent_msg_status_sent">изпратено</string> + <string name="icon_descr_sent_msg_status_send_failed">изпращането е неуспешно</string> + <string name="reset_verb">Нулиране</string> + <string name="send_verb">Изпрати</string> + <string name="add_contact_or_create_group">Започни нов чат</string> + <string name="send_us_an_email">Изпратете ни имейл</string> + <string name="smp_servers_test_some_failed">Някои сървъри не минаха теста:</string> + <string name="shutdown_alert_question">Изключване\?</string> + <string name="secret_text">таен</string> + <string name="alert_title_skipped_messages">Пропуснати съобщения</string> + <string name="self_destruct">Самоунищожение</string> + <string name="receipts_section_groups">Малки групи (максимум 20)</string> + <string name="receipts_groups_title_disable">Деактивиране на потвърждениe за доставка за групи\?</string> + <string name="receipts_groups_enable_for_all">Активиране за всички групи</string> + <string name="receipts_groups_override_enabled">Изпращането на потвърждениe за доставка е разрешено за %d групи</string> + <string name="restart_the_app_to_use_imported_chat_database">Рестартирайте приложението, за да използвате импортирана чат база данни.</string> + <string name="send_receipts_disabled_alert_msg">Тази група има над %1$d членове, потвърждениeто за доставка няма да се изпраща.</string> + <string name="conn_stats_section_title_servers">СЪРВЪРИ</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="delivery">Доставка</string> + <string name="receipts_groups_enable_keep_overrides">Активиране (запазване на груповите промени)</string> + <string name="send_receipts_disabled">деактивирано</string> + <string name="receipts_groups_disable_for_all">Деактивиране за всички групи</string> + <string name="receipts_groups_disable_keep_overrides">Деактивиране (запазване на груповите промени)</string> + <string name="send_receipts_disabled_alert_title">Потвърждениeто за доставка е деактивирано</string> + <string name="receipts_groups_override_disabled">Изпращането на потвърждениe за доставка е деактивирано за %d групи</string> + <string name="receipts_groups_title_enable">Aктивиране на потвърждениeто за доставка за групи\?</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Можете да стартирате чата през Настройки на приложението / Базата данни или като рестартирате приложението.</string> + <string name="revoke_file__confirm">Отзови</string> + <string name="revoke_file__action">Отзови файл</string> + <string name="stop_snd_file__message">Изпращането на файла ще бъде спряно.</string> + <string name="smp_servers_save">Запази сървърите</string> + <string name="send_live_message_desc">Изпратете съобщение на живо - то ще се актуализира за получателя(ите), докато го пишете</string> + <string name="smp_servers_test_failed">Тестът на сървъра е неуспешен!</string> + <string name="v4_2_security_assessment">Оценка на сигурността</string> + <string name="share_file">Сподели файл…</string> + <string name="sending_via">Изпращане чрез</string> + <string name="share_message">Сподели съобщение…</string> + <string name="color_sent_message">Изпратено съобщение</string> + <string name="set_group_preferences">Задай групови настройки</string> + <string name="share_image">Сподели медия…</string> + <string name="simplex_address">SimpleX адрес</string> + <string name="v4_2_security_assessment_desc">Сигурността на SimpleX Chat беше одитирана от Trail of Bits.</string> + <string name="settings_section_title_socks">SOCKS ПРОКСИ</string> + <string name="settings_restart_app">Рестартиране</string> + <string name="settings_shutdown">Изключване</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Рестартирайте приложението, за да създадете нов чат профил.</string> + <string name="restore_database_alert_title">Възстанови резервно копие на база данни\?</string> + <string name="restore_database">Възстанови резервно копие на база данни</string> + <string name="sender_at_ts">%s в %s</string> + <string name="current_version_timestamp">%s (текущ)</string> + <string name="button_send_direct_message">Изпрати лично съобщение</string> + <string name="save_and_update_group_profile">Запази и актуализирай профила на групата</string> + <string name="save_group_profile">Запази профила на групата</string> + <string name="accept_feature_set_1_day">Задай 1 ден</string> + <string name="sending_files_not_yet_supported">изпращането на файлове все още не се поддържа</string> + <string name="simplex_link_contact">SimpleX адрес за контакт</string> + <string name="simplex_link_group">SimpleX групов линк</string> + <string name="simplex_link_mode">SimpleX линкове</string> + <string name="smp_server_test_secure_queue">Сигурна опашка</string> + <string name="sender_cancelled_file_transfer">Подателят отмени прехвърлянето на файла.</string> + <string name="sender_may_have_deleted_the_connection_request">Подателят може да е изтрил заявката за връзка.</string> + <string name="error_smp_test_server_auth">Сървърът изисква оторизация за създаване на опашки, проверете паролата</string> + <string name="error_xftp_test_server_auth">Сървърът изисква оторизация за качване, проверете паролата</string> + <string name="ntf_channel_messages">SimpleX Chat съобщения</string> + <string name="save_verb">Запази</string> + <string name="search_verb">Търсене</string> + <string name="sent_message">Изпратено съобщение</string> + <string name="share_verb">Сподели</string> + <string name="auth_stop_chat">Спри чата</string> + <string name="reveal_verb">Покажи</string> + <string name="stop_file__action">Спри файл</string> + <string name="icon_descr_settings">Настройки</string> + <string name="star_on_github">Звезда в GitHub</string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[Задайте <i>Използване на .onion хостове</i> на Не, ако SOCKS проксито не ги поддържа.]]></string> + <string name="show_dev_options">Покажи:</string> + <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> + <string name="save_and_notify_contact">Запази и уведоми контакта</string> + <string name="save_and_notify_group_members">Запази и уведоми членовете на групата</string> + <string name="save_preferences_question">Запази настройките\?</string> + <string name="icon_descr_speaker_on">Високоговорителят е включен</string> + <string name="icon_descr_speaker_off">Високоговорителят е изключен</string> + <string name="stop_chat_to_enable_database_actions">Спрете чата, за да активирате действията с базата данни.</string> + <string name="role_in_group">Роля</string> + <string name="network_options_revert">Отмени промените</string> + <string name="network_options_save">Запази</string> + <string name="reset_color">Нулирай цветовете</string> + <string name="save_color">Запази цвета</string> + <string name="color_secondary">Вторичен</string> + <string name="custom_time_picker_select">Избери</string> + <string name="to_start_a_new_chat_help_header">За да започнете нов чат</string> + <string name="to_connect_via_link_title">За свързване чрез линк</string> + <string name="connection_you_accepted_will_be_cancelled">Връзката, която приехте, ще бъде отказана!</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Контактът, с когото споделихте този линк, НЯМА да може да се свърже!</string> + <string name="this_link_is_not_a_valid_connection_link">Този линк не е валиден линк за връзка!</string> + <string name="your_chat_profiles">Вашите чат профили</string> + <string name="smp_servers_per_user">Сървърите за нови връзки на текущия ви чат профил</string> + <string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата \"Вашите чат профили\".</string> + <string name="profile_is_only_shared_with_your_contacts">Профилът се споделя само с вашите контакти.</string> + <string name="delete_files_and_media_desc">Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени.</string> + <string name="tap_to_activate_profile">Докосни за активиране на профил.</string> + <string name="should_be_at_least_one_profile">Трябва да има поне един потребителски профил.</string> + <string name="should_be_at_least_one_visible_profile">Трябва да има поне един видим потребителски профил.</string> + <string name="language_system">Системен</string> + <string name="color_title">Заглавие</string> + <string name="to_share_with_your_contact">(за споделяне с вашия контакт)</string> + <string name="alert_message_no_group">Тази група вече не съществува.</string> + <string name="settings_section_title_support">ПОДКРЕПЕТЕ SIMPLEX CHAT</string> + <string name="contact_sent_large_file">Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%1$s).</string> + <string name="in_developing_desc">Тази функция все още не се поддържа. Опитайте следващата версия.</string> + <string name="tap_to_start_new_chat">Докосни за започване на нов чат</string> + <string name="this_text_is_available_in_settings">Този текст е достъпен в настройките</string> + <string name="images_limit_title">Твърде много изображения!</string> + <string name="this_QR_code_is_not_a_link">Този QR код не е линк!</string> + <string name="smp_servers_test_server">Тествай сървър</string> + <string name="network_session_mode_transport_isolation">Транспортна изолация</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Платформата за съобщения и приложения, защитаваща вашата поверителност и сигурност.</string> + <string name="switch_verb">Смени</string> + <string name="v4_4_french_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string> + <string name="v5_0_large_files_support">Видео и файлове до 1gb</string> + <string name="v4_6_chinese_spanish_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Това действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</string> + <string name="enable_automatic_deletion_message">Това действие не може да бъде отменено - съобщенията, изпратени и получени по-рано от избраното, ще бъдат изтрити. Може да отнеме няколко минути.</string> + <string name="database_backup_can_be_restored">Опитът за промяна на паролата на базата данни не беше завършен.</string> + <string name="group_is_decentralized">Групата е напълно децентрализирана – видима е само за членовете.</string> + <string name="v4_5_transport_isolation">Транспортна изолация</string> + <string name="v5_0_polish_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string> + <string name="alert_text_msg_bad_hash">Хешът на предишното съобщение е различен.</string> + <string name="smp_servers_test_servers">Тествай сървърите</string> + <string name="v4_5_italian_interface_descr">Благодарение на потребителите – допринесете през Weblate!</string> + <string name="v4_5_private_filenames_descr">За да не се разкрива часовата зона, файловете с изображения/глас използват UTC.</string> + <string name="this_string_is_not_a_connection_link">Този текст не е линк за връзка!</string> + <string name="videos_limit_title">Твърде много видеоклипове!</string> + <string name="messages_section_description">Тази настройка се прилага за съобщения в текущия ви профил</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">За да се защити поверителността, вместо потребителски идентификатори, използвани от всички други платформи, SimpleX има идентификатори за опашки от съобщения, отделни за всеки от вашите контакти.</string> + <string name="trying_to_connect_to_server_to_receive_messages">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %1$s).</string> + <string name="error_smp_test_failed_at_step">Тестът е неуспешен на стъпка %s.</string> + <string name="database_initialization_error_desc">Базата данни не работи правилно. Докоснете, за да научите повече</string> + <string name="image_decoding_exception_desc">Изображението не може да бъде декодирано. Моля, опитайте с друго изображение или се свържете с разработчиците.</string> + <string name="chat_help_tap_button">Докосни бутона</string> + <string name="thank_you_for_installing_simplex">Благодарим Ви, че инсталирахте SimpleX Chat!</string> + <string name="save_and_notify_contacts">Запази и уведоми контактите</string> + <string name="next_generation_of_private_messaging">Ново поколение поверителни съобщения</string> + <string name="first_platform_without_user_ids">Първата платформа без никакви потребителски идентификатори – поверителна по дизайн.</string> + <string name="la_mode_system">Системна</string> + <string name="alert_text_msg_bad_id">Неправилно ID на следващото съобщение (по-малко или еднакво с предишното). +\nТова може да се случи поради някаква грешка или когато връзката е компрометирана.</string> + <string name="whats_new_thanks_to_users_contribute_weblate">Благодарение на потребителите – допринесете през Weblate!</string> + <string name="unmute_chat">Уведомявай</string> + <string name="you_accepted_connection">Вие приехте връзката</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Ще бъдете свързани с групата, когато устройството на домакина на групата е онлайн, моля, изчакайте или проверете по-късно!</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Ще бъдете свързани, когато заявката ви за връзка бъде приета, моля, изчакайте или проверете по-късно!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Ще бъдете свързани, когато устройството на вашия контакт е онлайн, моля, изчакайте или проверете по-късно!</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Няма да загубите контактите си, ако по-късно изтриете адреса си.</string> + <string name="your_settings">Вашите настройки</string> + <string name="your_simplex_contact_address">Вашият SimpleX адрес</string> + <string name="smp_servers_use_server_for_new_conn">Използвай за нови връзки</string> + <string name="your_XFTP_servers">Вашите XFTP сървъри</string> + <string name="use_simplex_chat_servers__question">Използвай сървърите на SimpleX Chat\?</string> + <string name="using_simplex_chat_servers">Използват се сървърите на SimpleX Chat.</string> + <string name="your_ICE_servers">Вашите ICE сървъри</string> + <string name="your_SMP_servers">Вашите SMP сървъри</string> + <string name="network_socks_toggle_use_socks_proxy">Използвай SOCKS прокси</string> + <string name="network_enable_socks">Използвай SOCKS прокси\?</string> + <string name="update_onion_hosts_settings_question">Актуализиране на настройката за .onion хостове\?</string> + <string name="network_disable_socks">Използване на директна интернет връзка\?</string> + <string name="network_use_onion_hosts">Използвай .onion хостове</string> + <string name="network_use_onion_hosts_prefer">Когато са налични</string> + <string name="your_contacts_will_see_it">Вашите контакти в SimpleX ще го видят. +\nМожете да го промените в Настройки.</string> + <string name="your_profile_is_stored_on_your_device">Вашият профил, контакти и доставени съобщения се съхраняват на вашето устройство.</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Можете да използвате markdown за форматиране на съобщенията:</string> + <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Вие контролирате през кой сървър(и) <b>да получавате</b> съобщенията, вашите контакти – сървърите, които използвате, за да им изпращате съобщения.]]></string> + <string name="use_chat">Използвай чата</string> + <string name="update_database">Актуализация</string> + <string name="you_have_to_enter_passphrase_every_time">Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството.</string> + <string name="unknown_database_error_with_info">Неизвестна грешка в базата данни: %s</string> + <string name="unknown_error">Непозната грешка</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Ще спрете да получавате съобщения от тази група. Историята на чата ще бъде запазена.</string> + <string name="chat_preferences_yes">да</string> + <string name="chat_preferences_you_allow">Вие позволявате</string> + <string name="v4_3_voice_messages">Гласови съобщения</string> + <string name="view_security_code">Виж кода за сигурност</string> + <string name="voice_messages_prohibited">Гласовите съобщения са забранени!</string> + <string name="you_need_to_allow_to_send_voice">Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате.</string> + <string name="you_are_invited_to_group">Поканени сте в групата</string> + <string name="snd_conn_event_switch_queue_phase_completed">променихте адреса</string> + <string name="you_can_share_this_address_with_your_contacts">Можете да споделите този адрес с вашите контакти, за да им позволите да се свържат с %s.</string> + <string name="unfavorite_chat">Премахни от любимите</string> + <string name="settings_section_title_you">ВИЕ</string> + <string name="your_chat_database">Вашата чат база данни</string> + <string name="icon_descr_waiting_for_image">Изчаква се получаването на изображението</string> + <string name="waiting_for_image">Изчаква се получаването на изображението</string> + <string name="icon_descr_waiting_for_video">Изчаква се получаването на видеото</string> + <string name="icon_descr_video_snd_complete">Видеото е изпратено</string> + <string name="waiting_for_video">Изчаква се получаването на видеото</string> + <string name="video_will_be_received_when_contact_completes_uploading">Видеото ще бъде получено, когато вашият контакт завърши качването му.</string> + <string name="video_will_be_received_when_contact_is_online">Видеото ще бъде получено, когато вашият контакт е онлайн, моля, изчакайте или проверете по-късно!</string> + <string name="waiting_for_file">Изчаква се получаването на файла</string> + <string name="voice_message">Гласово съобщение</string> + <string name="voice_message_with_duration">Гласово съобщение (%1$s)</string> + <string name="voice_message_send_text">Гласово съобщение…</string> + <string name="voice_messages_are_prohibited">Гласовите съобщения са забранени в тази група.</string> + <string name="icon_descr_received_msg_status_unread">непрочетено</string> + <string name="welcome">Добре дошли!</string> + <string name="personal_welcome">Добре дошли %1$s!</string> + <string name="you_have_no_chats">Нямате чатове</string> + <string name="observer_cant_send_message_title">Не може да изпращате съобщения!</string> + <string name="smp_servers_your_server">Вашият сървър</string> + <string name="smp_servers_use_server">Използвай сървър</string> + <string name="smp_servers_your_server_address">Вашият адрес на сървъра</string> + <string name="you_can_create_it_later">Можете да го създадете по-късно</string> + <string name="your_contacts_will_remain_connected">Вашите контакти ще останат свързани.</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Ние не съхраняваме вашите контакти или съобщения (веднъж доставени) на сървърите.</string> + <string name="you_control_your_chat">Вие контролирате своя чат!</string> + <string name="wrong_passphrase_title">Грешна парола!</string> + <string name="voice_messages">Гласови съобщения</string> + <string name="your_preferences">Вашите настройки</string> + <string name="whats_new">Какво е новото</string> + <string name="v4_3_irreversible_message_deletion_desc">Вашите контакти могат да позволят пълното изтриване на съобщението.</string> + <string name="update_database_passphrase">Актуализирай паролата на базата данни</string> + <string name="snd_group_event_changed_role_for_yourself">променихте ролята си на %s</string> + <string name="snd_group_event_user_left">вие напуснахте</string> + <string name="snd_group_event_changed_member_role">променихте ролята на %s на %s</string> + <string name="snd_group_event_member_deleted">премахнахте %1$s</string> + <string name="incognito_random_profile">Вашият автоматично генериран профил</string> + <string name="user_unmute">Уведомявай</string> + <string name="you_can_share_your_address">Можете да споделите адреса си като линк или QR код - всеки може да се свърже с вас.</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">променихте адреса за %s</string> + <string name="group_main_profile_sent">Вашият чат профил ще бъде изпратен на членовете на групата</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Вашият чат профил ще бъде изпратен +\nдо вашия контакт</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Вашата текуща чат база данни ще бъде ИЗТРИТА и ЗАМЕНЕНА с импортираната. +\nТова действие не може да бъде отменено - вашият профил, контакти, съобщения и файлове ще бъдат безвъзвратно загубени.</string> + <string name="updating_settings_will_reconnect_client_to_all_servers">Актуализирането на настройките ще свърже отново клиента към всички сървъри.</string> + <string name="rcv_group_event_updated_group_profile">актуализиран профил на групата</string> + <string name="video_descr">Видео</string> + <string name="incognito_info_share">Когато споделяте инкогнито профил с някого, този профил ще се използва за групите, в които той ви кани.</string> + <string name="wrong_passphrase">Грешна парола за базата данни</string> + <string name="icon_descr_sent_msg_status_unauthorized_send">неоторизирано изпращане</string> + <string name="invite_prohibited_description">Опитвате се да поканите контакт, с когото сте споделили инкогнито профил, в групата, в която използвате основния си профил</string> + <string name="alert_title_cant_invite_contacts_descr">Използвате инкогнито профил за тази група - за да се предотврати споделянето на основния ви профил, поканите на контакти не са разрешени</string> + <string name="connected_to_server_to_receive_messages_from_contact">Вие сте свързани към сървъра, използван за получаване на съобщения от този контакт.</string> + <string name="profile_will_be_sent_to_contact_sending_link">Вашият профил ще бъде изпратен до контакта, от който сте получили този линк.</string> + <string name="unknown_message_format">непознат формат на съобщението</string> + <string name="simplex_link_connection">чрез %1$s</string> + <string name="simplex_link_mode_browser">Чрез браузър</string> + <string name="description_via_contact_address_link">чрез линк с адрес за контакт</string> + <string name="description_via_group_link">чрез групов линк</string> + <string name="sender_you_pronoun">вие</string> + <string name="description_you_shared_one_time_link">споделихте еднократен линк за връзка</string> + <string name="description_you_shared_one_time_link_incognito">споделихте еднократен инкогнито линк за връзка</string> + <string name="description_via_one_time_link">чрез еднократен линк за връзка</string> + <string name="connection_error_auth_desc">Освен ако вашият контакт не е изтрил връзката или този линк вече е бил използван, това може да е грешка - моля, докладвайте. +\nЗа да се свържете, моля, помолете вашия контакт да създаде друг линк за връзка и проверете дали имате стабилна мрежова връзка.</string> + <string name="smp_server_test_upload_file">Качи файл</string> + <string name="you_are_already_connected_to_vName_via_this_link">Вече сте вече свързани с %1$s.</string> + <string name="la_notice_turn_on">Включи</string> + <string name="auth_unlock">Отключи</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Ще трябва да се идентифицирате, когато стартирате или възобновите приложението след 30 секунди във фонов режим.</string> + <string name="you_are_observer">вие сте наблюдател</string> + <string name="gallery_video_button">Видео</string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Можете да <font color=#0088ff>се свържете с разработчиците на SimpleX Chat, за да задавате въпроси и да получавате актуализации</font>;.]]></string> + <string name="contact_wants_to_connect_with_you">иска да се свърже с вас!</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Можете също да се свържете, като натиснете върху линка. Ако се отвори в браузъра, натиснете върху бутона <b>Отваряне в мобилно приложение</b>.]]></string> + <string name="xftp_servers">XFTP сървъри</string> + <string name="update_network_session_mode_question">Актуализиране на режима на изолация на транспорта\?</string> + <string name="your_current_profile">Вашият текущ профил</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Вашият профил се съхранява на вашето устройство и се споделя само с вашите контакти. SimpleX сървърите не могат да видят вашия профил.</string> + <string name="icon_descr_video_off">Видеото е изключено</string> + <string name="icon_descr_video_on">Видеото е включено</string> + <string name="webrtc_ice_servers">WebRTC ICE сървъри</string> + <string name="your_ice_servers">Вашите ICE сървъри</string> + <string name="your_privacy">Вашата поверителност</string> + <string name="you_must_use_the_most_recent_version_of_database">Трябва да използвате най-новата версия на вашата чат база данни САМО на едно устройство, в противен случай може да спрете да получавате съобщения от някои контакти.</string> + <string name="group_info_member_you">вие: %1$s</string> + <string name="update_network_settings_confirmation">Актуализация</string> + <string name="update_network_settings_question">Актуализиране на мрежовите настройки\?</string> + <string name="voice_prohibited_in_this_chat">Гласовите съобщения са забранени в този чат.</string> + <string name="v5_1_better_messages_descr">- гласови съобщения до 5 минути. +\n- персонализирано време за изчезване. +\n- история на редактиране.</string> + <string name="connect_via_link_incognito">Свързване инкогнито</string> + <string name="turn_off_system_restriction_button">Отвори настройките на приложението</string> + <string name="turn_off_battery_optimization_button">Позволи</string> + <string name="connect__a_new_random_profile_will_be_shared">Нов автоматично генериран профил ще бъде споделен.</string> + <string name="disable_notifications_button">Деактивирай известията</string> + <string name="system_restricted_background_in_call_title">Без фонови разговори</string> + <string name="connect_via_member_address_alert_title">Свързване директно\?</string> + <string name="connect_use_current_profile">Използвай текущия профил</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Поставете линка, който сте получили, за да се свържете с вашия контакт…</string> + <string name="connect__your_profile_will_be_shared">Вашият профил %1$s ще бъде споделен.</string> + <string name="system_restricted_background_desc">SimpleX не може да работи във фонов режим. Ще получавате известията само когато приложението работи.</string> + <string name="system_restricted_background_in_call_desc">Приложението може да се затвори след 1 минута във фонов режим.</string> + <string name="system_restricted_background_warn"><![CDATA[За да активирате известията, моля, изберете <b>Използване на батерията на приложението</b> / <b>Неограничено</b> в системните настройки на приложението.]]></string> + <string name="connect_use_new_incognito_profile">Използвай нов инкогнито профил</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[За да извършвате разговори във фонов режим, моля, изберете <b>Използване на батерията на приложението</b> / <b>Неограничено</b> в системните настройки на приложението.]]></string> + <string name="you_invited_a_contact">Вие поканихте контакта</string> + <string name="connect_via_member_address_alert_desc">Заявката за свързване ще бъде изпратена до този член на групата.</string> + <string name="rcv_group_event_2_members_connected">%s и %s са свързани</string> + <string name="rcv_group_event_n_members_connected">%s, %s и %d други членове са свързани</string> + <string name="rcv_group_event_3_members_connected">%s, %s и %s са свързани</string> + <string name="privacy_message_draft">Чернова на съобщение</string> + <string name="privacy_show_last_messages">Показване на последните съобщения в листа с чатовете</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Базата данни ще бъде криптирана и паролата ще бъде съхранена в настройките.</string> + <string name="you_can_change_it_later">Автоматично генерирана парола се съхранява в настройките като обикновен текст. +\nМожете да я промените по-късно.</string> + <string name="database_encryption_will_be_updated_in_settings">Паролата за криптиране на базата данни ще бъде актуализирана и съхранена в настройките.</string> + <string name="remove_passphrase_from_settings">Премахване на паролата от настройките\?</string> + <string name="use_random_passphrase">Използвай автоматично генерирана парола</string> + <string name="save_passphrase_in_settings">Запази паролата в настройките</string> + <string name="setup_database_passphrase">Задай парола за базата данни</string> + <string name="set_database_passphrase">Задай парола за базата данни</string> + <string name="open_database_folder">Отвори папката за база данни</string> + <string name="passphrase_will_be_saved_in_settings">Паролата ще бъде съхранена в настройките като обикновен текст, след като я промените или рестартирате приложението.</string> + <string name="settings_is_storing_in_clear_text">Паролата се съхранява в настройките като обикновен текст.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Моля, обърнете внимание</b>: релетата за съобщения и файлове са свързани чрез SOCKS прокси. Обажданията и изпращането на визуализации на линкове използват директна връзка.]]></string> + <string name="encrypt_local_files">Криптиране на локални файлове</string> + <string name="v5_3_encrypt_local_files">Криптиране на съхранените файлове и медия</string> + <string name="v5_3_new_desktop_app">Ново настолно приложение!</string> + <string name="v5_3_new_interface_languages">6 нови езика на интерфейса</string> + <string name="v5_3_encrypt_local_files_descr">Приложението криптира нови локални файлове (с изключение на видеоклипове).</string> + <string name="v5_3_discover_join_groups">Открийте и се присъединете към групи</string> + <string name="v5_3_simpler_incognito_mode">Опростен режим инкогнито</string> + <string name="v5_3_new_interface_languages_descr">Арабски, български, финландски, иврит, тайландски и украински - благодарение на потребителите и Weblate.</string> + <string name="v5_3_new_desktop_app_descr">Създайте нов профил в настолното приложение. 💻</string> + <string name="v5_3_simpler_incognito_mode_descr">Избор на инкогнито при свързване.</string> + <string name="v5_3_discover_join_groups_descr">- свържете се с директория за услуги (БЕТА)! +\n- потвърждениe за доставка (до 20 члена). +\n- по-бързо и по-стабилно.</string> </resources> \ No newline at end of file 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 9576c1ff92..2afdb7a690 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -55,7 +55,6 @@ <string name="update_network_settings_question">Aktualizovat nastavení sítě\?</string> <string name="incognito">Inkognito</string> <string name="incognito_random_profile">Váš náhodný profil</string> - <string name="incognito_random_profile_description">Vašemu kontaktu bude zaslán náhodný profil</string> <string name="save_color">Uložit barvu</string> <string name="reset_color">Obnovit barvu</string> <string name="color_primary">Zbarvení</string> @@ -201,7 +200,7 @@ <string name="mark_unread">Označit jako nepřečteno</string> <string name="mute_chat">Ztlumit</string> <string name="unmute_chat">Zrušit ztlumení</string> - <string name="you_invited_your_contact">Pozvali jste kontakt</string> + <string name="you_invited_a_contact">Pozvali jste kontakt</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">Kontakt, se kterým jste tento odkaz sdíleli, se NEBUDE moci připojit!</string> <string name="connection_you_accepted_will_be_cancelled">Připojení, které jste přijali, bude zrušeno!</string> <string name="icon_descr_help">help</string> @@ -210,7 +209,6 @@ <string name="you_will_be_connected_when_group_host_device_is_online">Ke skupině budete připojeni, až bude zařízení hostitele skupiny online, vyčkejte prosím nebo se podívejte později!</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">Budete připojeni, jakmile bude vaše žádost o připojení přijata, vyčkejte prosím nebo se podívejte později!</string> <string name="connection_request_sent">Požadavek na připojení byl odeslán!</string> - <string name="your_profile_will_be_sent">Váš chat profil bude odeslán vašemu kontaktu</string> <string name="create_one_time_link">Vytvořit jednorázovou pozvánku</string> <string name="one_time_link">Jednorázová pozvánka</string> <string name="security_code">Bezpečnostní kód</string> @@ -875,7 +873,6 @@ <string name="network_status">Stav sítě</string> <string name="switch_receiving_address">Přepínač přijímací adresy</string> <string name="group_is_decentralized">Skupina je plně decentralizovaná - viditelná pouze pro členy.</string> - <string name="group_unsupported_incognito_main_profile_sent">Zde není podporován režim inkognito - členům skupiny bude zaslán váš hlavní profil.</string> <string name="save_group_profile">Uložení profilu skupiny</string> <string name="network_options_reset_to_defaults">Obnovit výchozí nastavení</string> <string name="network_option_tcp_connection_timeout">Časový limit připojení TCP</string> @@ -888,9 +885,7 @@ <string name="users_delete_profile_for">Smazat chat profil pro</string> <string name="users_delete_with_connections">Profil a připojení k serveru</string> <string name="users_delete_data_only">Pouze místní data profilu</string> - <string name="incognito_random_profile_from_contact_description">Náhodný profil bude zaslán kontaktu, od kterého jste obdrželi tento odkaz.</string> - <string name="incognito_info_protects">Režim inkognito chrání soukromí vašeho hlavního profilu, jména a obrázku - pro každý nový kontakt je vytvořen nový náhodný profil.</string> - <string name="incognito_info_find">Chcete-li najít profil použitý pro inkognito připojení, klepněte na název kontaktu nebo skupiny v horní části.</string> + <string name="incognito_info_protects">Režim inkognito chrání vaše soukromí používáním nového náhodného profilu pro každý kontakt.</string> <string name="theme_light">Světlé</string> <string name="theme_dark">Tmavé</string> <string name="theme">Téma</string> @@ -951,7 +946,6 @@ <string name="your_chat_profile_will_be_sent_to_your_contact">Váš chat profil bude odeslán \nvašemu kontaktu</string> <string name="your_chats">Konverzace</string> - <string name="paste_connection_link_below_to_connect">Do níže uvedeného pole vložte odkaz, který jste obdrželi pro spojení s kontaktem.</string> <string name="share_invitation_link">Sdílet jednorázovou pozvánku</string> <string name="status_e2e_encrypted">koncově šifrované</string> <string name="moderated_description">moderované</string> @@ -1078,14 +1072,14 @@ <string name="enable_lock">Povolit zámek</string> <string name="lock_after">Zamknout po</string> <string name="lock_mode">Režim zámku</string> - <string name="authentication_cancelled">Ověření zrušeno</string> + <string name="authentication_cancelled">Autentizace zrušena</string> <string name="confirm_passcode">Potvrdit heslo</string> <string name="incorrect_passcode">Nesprávné heslo</string> <string name="new_passcode">Nové heslo</string> <string name="submit_passcode">Odeslat</string> <string name="la_mode_system">Systém</string> <string name="change_lock_mode">Změnit zamykání</string> - <string name="la_mode_passcode">Heslo</string> + <string name="la_mode_passcode">Přístupový kód</string> <string name="passcode_changed">Heslo změněno!</string> <string name="passcode_not_changed">Heslo nezměněno!</string> <string name="passcode_set">Heslo nastaveno!</string> @@ -1220,7 +1214,7 @@ <string name="v5_1_self_destruct_passcode">Samodestrukční heslo</string> <string name="v5_1_japanese_portuguese_interface">Japonské a portugalské uživatelské rozhraní</string> <string name="custom_time_unit_minutes">minut</string> - <string name="custom_time_unit_seconds">vteřiny</string> + <string name="custom_time_unit_seconds">vteřin</string> <string name="whats_new_thanks_to_users_contribute_weblate">Díky uživatelům - překládejte prostřednictvím Weblate!</string> <string name="v5_1_better_messages_descr">- 5 minutové hlasové zprávy. \n- vlastní čas mizení. @@ -1338,4 +1332,62 @@ <string name="files_and_media_prohibited">Soubory a média jsou zakázány!</string> <string name="in_reply_to">V odpovědi na</string> <string name="no_history">Žádná historie</string> + <string name="network_option_protocol_timeout_per_kb">Časový limit protokolu na KB</string> + <string name="settings_section_title_delivery_receipts">ZASLAT POTVRZENÍ O DORUČENÍ NA</string> + <string name="v5_2_message_delivery_receipts_descr">Druhé zaškrtnutí jsme přehlédli! ✅</string> + <string name="switch_receiving_address_desc">Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele.</string> + <string name="choose_file_title">Vybrat soubor</string> + <string name="connect_via_link_incognito">Spojit se inkognito</string> + <string name="turn_off_battery_optimization_button">Povolit</string> + <string name="disable_notifications_button">Vypnout upozornění</string> + <string name="turn_off_system_restriction_button">Otevřít nastavení aplikace</string> + <string name="system_restricted_background_in_call_title">Žádné volání na pozadí</string> + <string name="receipts_groups_enable_keep_overrides">Povolit (zachovat nastavení skupin)</string> + <string name="receipts_groups_disable_keep_overrides">Vypnout (zachovat nastavení skupin)</string> + <string name="connect_via_member_address_alert_title">Připojit se přímo\?</string> + <string name="connect__a_new_random_profile_will_be_shared">Nový náhodný profil bude sdílen.</string> + <string name="receipts_groups_enable_for_all">Povolit pro všechny skupiny</string> + <string name="no_selected_chat">Žádný vybraný chat</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Vložte odkaz který jste obdrželi, pro spojení se svým kontaktem…</string> + <string name="no_info_on_delivery">Žádné informace o doručení</string> + <string name="receipts_groups_override_disabled">Odesílání doručenky je zakázáno pro %d skupin</string> + <string name="receipts_groups_disable_for_all">Vypnout pro všechny skupiny</string> + <string name="send_receipts_disabled">vypnut</string> + <string name="send_receipts_disabled_alert_title">Receipts jsou zakázány</string> + <string name="in_developing_title">Již brzy!</string> + <string name="connect_use_current_profile">Použít aktuální profil</string> + <string name="connect_use_new_incognito_profile">Použít nový incognito profil</string> + <string name="system_restricted_background_desc">SimpleX nemůže běžet na pozadí. Pouze při spuštěné aplikaci obdržíte upozornění.</string> + <string name="system_restricted_background_warn"><![CDATA[Chcete-li povolit oznámení, vyberte prosím <b>Baterii</b> / <b>bez omezení</b> v nastavení aplikace.]]></string> + <string name="system_restricted_background_in_call_desc">Aplikace může být uzavřena po 1 minutě na pozadí.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Chcete-li volat na pozadí, vyberte prosím <b>Baterii</b> / <b>bez omezení</b> v nastavení aplikace.]]></string> + <string name="connect__your_profile_will_be_shared">Váš profil %1$s bude sdílen.</string> + <string name="connect_via_member_address_alert_desc">Požadavek na připojení bude zaslán tomuto členu skupiny.</string> + <string name="delivery">Doručenka</string> + <string name="receipts_groups_title_disable">Zakázat doručenky pro skupiny\?</string> + <string name="receipts_groups_title_enable">Povolit doručenky pro skupiny\?</string> + <string name="receipts_groups_override_enabled">Odeslání doručenek je povoleno pro %d skupiny</string> + <string name="receipts_section_groups">Malé skupiny (max. 20)</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="rcv_group_event_2_members_connected">%s a %s připojen</string> + <string name="rcv_group_event_3_members_connected">%s, %s a %s připojeni</string> + <string name="rcv_group_event_n_members_connected">%s, %s a %d dalších členů připojeno</string> + <string name="privacy_message_draft">Rozepsáno</string> + <string name="privacy_show_last_messages">Zobrazit poslední zprávy</string> + <string name="send_receipts_disabled_alert_msg">Tato skupina má více než %1$d členů, doručenky nejsou odeslány.</string> + <string name="in_developing_desc">Tato funkce zatím není podporována. Vyzkoušejte další vydání.</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Databáze bude zašifrována a heslo bude uloženo v klíčence.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Všimněte si prosím</b>: zprávy a relé souborů jsou spojeny prostřednictvím proxy SOCKS. Volání a odesílání náhledů odkazů pomocí přímého připojení.]]></string> + <string name="encrypt_local_files">Šifrovat místní soubory</string> + <string name="you_can_change_it_later">Náhodné heslo je uloženo v nastavení jako prostý text. +\nMůžete jej změnit později.</string> + <string name="database_encryption_will_be_updated_in_settings">Heslo pro šifrování databáze bude aktualizováno a uloženo v klíčence.</string> + <string name="remove_passphrase_from_settings">Odebrat heslo z nastavení\?</string> + <string name="use_random_passphrase">Použít náhodné heslo</string> + <string name="save_passphrase_in_settings">Uložit heslo v nastavení</string> + <string name="setup_database_passphrase">Nastavení hesla databáze</string> + <string name="set_database_passphrase">Nastavit heslo databáze</string> + <string name="open_database_folder">Otevřete složku databáze</string> + <string name="passphrase_will_be_saved_in_settings">Heslo bude uloženo v nastavení jako prostý text až jej změníte nebo po restartu aplikace.</string> + <string name="settings_is_storing_in_clear_text">Heslo je uloženo v nastavení jako prostý text.</string> </resources> \ No newline at end of file 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 159dd1e5dc..6fad3dc0ac 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -4,17 +4,17 @@ <string name="thousand_abbreviation">k</string> <!-- Connect via Link - MainActivity.kt --> <string name="connect_via_contact_link">Über den Kontakt-Link verbinden?</string> - <string name="connect_via_invitation_link">Über den Einladungs-Link verbinden?</string> + <string name="connect_via_invitation_link">Über den Einladungslink verbinden\?</string> <string name="connect_via_group_link">Über den Gruppen-Link verbinden?</string> - <string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet von dem Sie diesen Link erhalten haben.</string> - <string name="you_will_join_group">Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht und sich mit deren Gruppenmitgliedern verbinden.</string> + <string name="profile_will_be_sent_to_contact_sending_link">Ihr Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string> + <string name="you_will_join_group">Sie werden der Gruppe beitreten, auf die sich dieser Link bezieht, und sich mit deren Gruppenmitgliedern verbinden.</string> <string name="connect_via_link_verb">Verbinden</string> <!-- Server info - ChatModel.kt --> <string name="server_connected">Verbunden</string> <string name="server_error">Fehler</string> <string name="server_connecting">Verbinde</string> <string name="connected_to_server_to_receive_messages_from_contact">Sie sind mit dem Server verbunden, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string> - <string name="trying_to_connect_to_server_to_receive_messages_with_error">Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s).</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Beim Versuch, die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %1$s).</string> <string name="trying_to_connect_to_server_to_receive_messages">Versuche die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird.</string> <!-- Item Content - ChatModel.kt --> <string name="deleted_description">Gelöscht</string> @@ -30,7 +30,7 @@ <string name="display_name_invited_to_connect">für eine Verbindung eingeladen</string> <string name="display_name_connecting">verbinde…</string> <string name="description_you_shared_one_time_link">sie haben einen Einmal-Link geteilt</string> - <string name="description_you_shared_one_time_link_incognito">sie haben Inkognito einen Einmal-Link geteilt</string> + <string name="description_you_shared_one_time_link_incognito">sie haben einen Einmal-Link inkognito geteilt</string> <string name="description_via_group_link">über einen Gruppen-Link</string> <string name="description_via_group_link_incognito">Inkognito über einen Gruppen-Link</string> <string name="description_via_contact_address_link">über einen Kontaktadressen-Link</string> @@ -38,18 +38,18 @@ <string name="description_via_one_time_link">über einen Einmal-Link</string> <string name="description_via_one_time_link_incognito">Inkognito über einen Einmal-Link</string> <!-- FormattedText, SimpleX links - ChatModel.kt --> - <string name="simplex_link_contact">SimpleX Kontaktadressen-Link</string> - <string name="simplex_link_invitation">SimpleX Einmal-Link</string> - <string name="simplex_link_group">SimpleX Gruppen-Link</string> + <string name="simplex_link_contact">SimpleX-Kontaktadressen-Link</string> + <string name="simplex_link_invitation">SimpleX-Einmal-Einladung</string> + <string name="simplex_link_group">SimpleX-Gruppen-Link</string> <string name="simplex_link_connection">über %1$s</string> <string name="simplex_link_mode">SimpleX-Links</string> <string name="simplex_link_mode_description">Beschreibung</string> <string name="simplex_link_mode_full">Vollständiger Link</string> <string name="simplex_link_mode_browser">Über den Browser</string> - <string name="simplex_link_mode_browser_warning">Das Öffnen des Links über den Browser kann die Privatsphäre und Sicherheit der Verbindung reduzieren. SimpleX-Links, denen nicht vertraut wird, werden Rot sein.</string> + <string name="simplex_link_mode_browser_warning">Das Öffnen des Links über den Browser kann die Privatsphäre und Sicherheit der Verbindung reduzieren. SimpleX-Links, denen nicht vertraut wird, werden rot markiert.</string> <!-- SimpleXAPI.kt --> <string name="error_saving_smp_servers">Fehler beim Speichern der SMP-Server</string> - <string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string> + <string name="ensure_smp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die SMP-Server-Adressen das richtige Format haben, zeilenweise getrennt und nicht doppelt vorhanden sind.</string> <string name="error_setting_network_config">Fehler bei der Aktualisierung der Netzwerk-Konfiguration.</string> <!-- API Error Responses - SimpleXAPI.kt --> <string name="connection_timeout">Verbindungszeitüberschreitung</string> @@ -65,9 +65,10 @@ <string name="contact_already_exists">Kontakt ist bereits vorhanden</string> <string name="you_are_already_connected_to_vName_via_this_link">Sie sind bereits mit %1$s verbunden.</string> <string name="invalid_connection_link">Ungültiger Verbindungslink</string> - <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben, oder bitten Sie Ihren Kontakt darum, Ihnen nochmal einen Link zuzusenden.</string> <string name="connection_error_auth">Verbindungsfehler (AUTH)</string> - <string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln - Bitte melden Sie es uns.\nBitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben.</string> + <string name="connection_error_auth_desc">Entweder hat Ihr Kontakt die Verbindung gelöscht, oder dieser Link wurde bereits verwendet, es könnte sich um einen Fehler handeln – bitte melden Sie ihn uns. +\nBitten Sie Ihren Kontakt darum, einen weiteren Verbindungs-Link zu erzeugen, um sich neu verbinden zu können, und stellen Sie sicher, dass Sie eine stabile Netzwerk-Verbindung haben.</string> <string name="error_accepting_contact_request">Fehler beim Akzeptieren der Kontaktanfrage</string> <string name="sender_may_have_deleted_the_connection_request">Der Absender hat möglicherweise die Verbindungsanfrage gelöscht.</string> <string name="error_deleting_contact">Fehler beim Löschen des Kontakts</string> @@ -76,7 +77,7 @@ <string name="error_deleting_pending_contact_connection">Fehler beim Löschen der anstehenden Kontaktaufnahme</string> <string name="error_changing_address">Fehler beim Wechseln der Adresse</string> <string name="error_smp_test_failed_at_step">Der Test ist beim Schritt %s fehlgeschlagen.</string> - <string name="error_smp_test_server_auth">Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort.</string> + <string name="error_smp_test_server_auth">Um Warteschlangen zu erzeugen, benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort.</string> <string name="error_smp_test_certificate">Der Fingerabdruck des Zertifikats in der Serveradresse ist wahrscheinlich ungültig.</string> <string name="smp_server_test_connect">Verbinde</string> <string name="smp_server_test_create_queue">Erzeuge Warteschlange</string> @@ -87,9 +88,9 @@ <string name="icon_descr_instant_notifications">Sofortige Benachrichtigungen</string> <string name="service_notifications">Sofortige Benachrichtigungen!</string> <string name="service_notifications_disabled">Sofortige Benachrichtigungen sind deaktiviert!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Um Ihre Privatsphäre zu schützen kann statt der Push-Benachrichtigung der <b>SimpleX Hintergrunddienst genutzt werden</b> – dieser benötigt ein paar Prozent Akkuleistung am Tag.]]></string> - <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Diese können über die Einstellungen deaktiviert werden</b> – Solange die App abläuft werden Benachrichtigungen weiterhin angezeigt.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[Um diese Funktion zu nutzen, ist es nötig, die Einstellung <b>Akkuoptimierung</b> für SimpleX im nächsten Dialog zu <b>deaktivieren</b>. Ansonsten werden die Benachrichtigungen deaktiviert.]]></string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Um Ihre Privatsphäre zu schützen, kann statt der Push-Benachrichtigung der <b>SimpleX-Hintergrunddienst genutzt werden</b> – dieser benötigt ein paar Prozent Akkuleistung am Tag.]]></string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Diese können über die Einstellungen deaktiviert werden</b> – solange die App läuft, werden Benachrichtigungen weiterhin angezeigt.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Um diese Funktion zu nutzen, wählen Sie im nächsten Dialog bitte die Einstellung <b>Erlauben Sie SimpleX im Hintergrund abzulaufen</b>. Ansonsten werden die Benachrichtigungen deaktiviert.]]></string> <string name="turning_off_service_and_periodic">Die Akkuoptimierung ist aktiv, der Hintergrunddienst und die periodische Nachfrage nach neuen Nachrichten ist abgeschaltet. Sie können diese Funktion in den Einstellungen wieder aktivieren.</string> <string name="periodic_notifications">Periodische Benachrichtigungen</string> <string name="periodic_notifications_disabled">Periodische Benachrichtigungen sind deaktiviert!</string> @@ -99,21 +100,21 @@ <string name="database_initialization_error_title">Die Datenbank kann nicht initialisiert werden</string> <string name="database_initialization_error_desc">Die Datenbank arbeitet nicht richtig. Tippen Sie für weitere Informationen.</string> <!-- SimpleX Chat foreground Service --> - <string name="simplex_service_notification_title">SimpleX Chat Hintergrunddienst</string> + <string name="simplex_service_notification_title">SimpleX-Chat-Hintergrunddienst</string> <string name="simplex_service_notification_text">Empfange Nachrichten …</string> <string name="hide_notification">Verberge</string> <!-- Notification channels --> - <string name="ntf_channel_messages">SimpleX Chat Nachrichten</string> - <string name="ntf_channel_calls">SimpleX Chat Anrufe</string> + <string name="ntf_channel_messages">SimpleX-Chat-Nachrichten</string> + <string name="ntf_channel_calls">SimpleX-Chat-Anrufe</string> <!-- Notifications --> <string name="settings_notifications_mode_title">Benachrichtigungsdienst</string> <string name="settings_notification_preview_mode_title">Vorschau anzeigen</string> <string name="settings_notification_preview_title">Benachrichtigungsvorschau</string> - <string name="notifications_mode_off">Wird ausgeführt, wenn die App geöffnet ist</string> - <string name="notifications_mode_periodic">Startet periodisch</string> + <string name="notifications_mode_off">Nur bei geöffneter App aktiv</string> + <string name="notifications_mode_periodic">Periodisch aktiv</string> <string name="notifications_mode_service">Immer aktiv</string> <string name="notifications_mode_off_desc">Die App kann Benachrichtigungen nur empfangen, wenn sie ausgeführt wird, es wird kein Hintergrunddienst gestartet.</string> - <string name="notifications_mode_periodic_desc">Überprüft alle 10 Minuten auf neue Nachrichten für bis zu einer Minute.</string> + <string name="notifications_mode_periodic_desc">Überprüft alle 10 Minuten auf neue Nachrichten für bis zu eine Minute.</string> <string name="notifications_mode_service_desc">Der Hintergrunddienst läuft immer – Benachrichtigungen werden angezeigt, sobald Nachrichten verfügbar sind.</string> <string name="notification_preview_mode_message">Nachrichtentext</string> <string name="notification_preview_mode_contact">Kontaktname</string> @@ -126,20 +127,20 @@ <string name="notification_new_contact_request">Neue Kontaktanfrage</string> <string name="notification_contact_connected">Verbunden</string> <!-- local authentication notice - SimpleXAPI.kt --> - <string name="la_notice_title_simplex_lock">SimpleX Sperre</string> + <string name="la_notice_title_simplex_lock">SimpleX-Sperre</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Um Ihre Informationen zu schützen, schalten Sie die SimpleX Sperre ein.\nSie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird.</string> <string name="la_notice_turn_on">Aktivieren</string> <!-- LocalAuthentication.kt --> - <string name="auth_simplex_lock_turned_on">SimpleX Sperre aktiviert</string> + <string name="auth_simplex_lock_turned_on">SimpleX-Sperre aktiviert</string> <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Sie müssen sich authentifizieren, wenn Sie die im Hintergrund befindliche App nach 30 Sekunden starten oder fortsetzen.</string> <string name="auth_unlock">Entsperren</string> <string name="auth_log_in_using_credential">Melden Sie sich mit Ihren Zugangsdaten an</string> - <string name="auth_enable_simplex_lock">SimpleX Sperre aktivieren</string> - <string name="auth_disable_simplex_lock">SimpleX Sperre deaktivieren</string> + <string name="auth_enable_simplex_lock">SimpleX-Sperre aktivieren</string> + <string name="auth_disable_simplex_lock">SimpleX-Sperre deaktivieren</string> <string name="auth_confirm_credential">Bestätigen Sie Ihre Zugangsdaten</string> <string name="auth_unavailable">Authentifizierung nicht verfügbar</string> - <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Geräteauthentifizierung ist nicht aktiviert. Sie können die SimpleX Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</string> - <string name="auth_device_authentication_is_disabled_turning_off">Geräteauthentifizierung ist deaktiviert. SimpleX Sperre ist abgeschaltet.</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Geräteauthentifizierung ist nicht aktiviert. Sie können die SimpleX-Sperre über die Einstellungen aktivieren, sobald Sie die Geräteauthentifizierung aktiviert haben.</string> + <string name="auth_device_authentication_is_disabled_turning_off">Geräteauthentifizierung ist deaktiviert. SimpleX-Sperre wird ausgeschaltet.</string> <string name="auth_stop_chat">Chat beenden</string> <string name="auth_open_chat_console">Chat-Konsole öffnen</string> <!-- Chat Alerts - ChatItemView.kt --> @@ -156,7 +157,7 @@ <string name="hide_verb">Verbergen</string> <string name="allow_verb">Erlauben</string> <string name="delete_message__question">Die Nachricht löschen?</string> - <string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht – dies kann nicht rückgängig gemacht werden!</string> <string name="delete_message_mark_deleted_warning">Die Nachricht wird zum Löschen markiert. Der/die Empfänger kann/können diese Nachricht aufdecken.</string> <string name="for_me_only">Für mich löschen</string> <string name="for_everybody">Für alle</string> @@ -190,7 +191,7 @@ <string name="images_limit_title">Zu viele Bilder!</string> <string name="images_limit_desc">Es können nur 10 Bilder auf einmal gesendet werden</string> <string name="image_decoding_exception_title">Dekodierungsfehler</string> - <string name="image_decoding_exception_desc">Das Bild kann nicht dekodiert werden. Bitte versuchen Sie es mit einem anderen Bild oder wenden Sie sich an die Entwickler.</string> + <string name="image_decoding_exception_desc">Das Bild kann nicht dekodiert werden. Bitte versuchen Sie es mit einem anderen Bild, oder wenden Sie sich an die Entwickler.</string> <!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt --> <string name="image_descr">Bild</string> <string name="icon_descr_waiting_for_image">Warten auf ein Bild</string> @@ -205,7 +206,7 @@ <string name="contact_sent_large_file">Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%1$s).</string> <string name="maximum_supported_file_size">Die derzeit maximal unterstützte Dateigröße beträgt %1$s.</string> <string name="waiting_for_file">Warte auf Datei</string> - <string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später nochmal nach!</string> + <string name="file_will_be_received_when_contact_is_online">Die Datei wird empfangen, sobald Ihr Kontakt online ist. Bitte warten oder schauen Sie später noch mal nach!</string> <string name="file_saved">Datei gespeichert</string> <string name="file_not_found">Datei nicht gefunden</string> <string name="error_saving_file">Fehler beim Speichern der Datei</string> @@ -217,7 +218,7 @@ <string name="notifications">Benachrichtigungen</string> <!-- Chat Info Actions - ChatInfoView.kt --> <string name="delete_contact_question">Kontakt löschen?</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Der Kontakt und alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Der Kontakt und alle Nachrichten werden gelöscht – dies kann nicht rückgängig gemacht werden!</string> <string name="button_delete_contact">Kontakt löschen</string> <string name="text_field_set_contact_placeholder">Kontaktname festlegen…</string> <string name="icon_descr_server_status_connected">Verbunden</string> @@ -245,7 +246,7 @@ <string name="copied">In die Zwischenablage kopiert</string> <!-- NewChatSheet --> <string name="add_contact_or_create_group">Neuen Chat starten</string> - <string name="share_one_time_link">Erstellen Sie einen einmaligen Einladungslink</string> + <string name="share_one_time_link">Einmal-Einladungslink teilen</string> <string name="connect_via_link_or_qr">Per Link / QR-Code verbinden</string> <string name="scan_QR_code">QR-Code scannen</string> <string name="create_group">Geheime Gruppe erstellen</string> @@ -261,16 +262,16 @@ <string name="gallery_video_button">Video</string> <!-- help - ChatHelpView.kt --> <string name="thank_you_for_installing_simplex">Danke, dass Sie SimpleX Chat installiert haben!</string> - <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Sie können sich <font color="#0088ff">mit SimpleX Chat Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Sie können sich <font color=#0088ff>mit SimpleX-Chat-Entwicklern verbinden, um Fragen zu stellen und Updates zu erhalten</font>.]]></string> <string name="to_start_a_new_chat_help_header">Um einen neuen Chat zu starten</string> <string name="chat_help_tap_button">Schaltfläche antippen</string> <string name="above_then_preposition_continuation">Danach die gewünschte Aktion auswählen:</string> <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Neuen Kontakt hinzufügen</b>: Um Ihren Einmal-QR-Code für Ihren Kontakt zu erstellen.]]></string> <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR-Code scannen</b>: Um sich mit Ihrem Kontakt zu verbinden, der Ihnen seinen QR-Code zeigt.]]></string> <string name="to_connect_via_link_title">Über Link verbinden</string> - <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Wenn Sie einen SimpleX Chat Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen:</string> + <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Wenn Sie einen SimpleX-Chat-Einladungslink erhalten haben, können Sie ihn in Ihrem Browser öffnen:</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 Desktop: Angezeigten QR-Code aus der App scannen, über <b>QR-Code scannen</b>.]]></string> - <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 Handy: Tippen Sie auf <b>In mobiler App öffnen</b> und dann auf <b>Verbinden</b> in der App.]]></string> + <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 Handy: Tippen Sie in der App auf „<b>In mobiler App öffnen</b>“ und dann auf „<b>Verbinden</b>“.]]></string> <!-- Contact Request Alert Dialogue - ChatListNavLinkView.kt --> <string name="accept_connection_request__question">Die Verbindungsanfrage akzeptieren?</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Wenn Sie ablehnen, wird der Absender NICHT benachrichtigt.</string> @@ -292,7 +293,7 @@ <string name="mute_chat">Stummschalten</string> <string name="unmute_chat">Stummschaltung aufheben</string> <!-- Pending contact connection alert dialogues --> - <string name="you_invited_your_contact">Sie haben Ihren Kontakt eingeladen</string> + <string name="you_invited_a_contact">Sie haben Ihren Kontakt eingeladen</string> <string name="you_accepted_connection">Sie haben die Verbindung akzeptiert</string> <string name="delete_pending_connection__question">Ausstehende Verbindung löschen?</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">Der Kontakt, mit dem Sie diesen Link geteilt haben, kann sich NICHT verbinden!</string> @@ -306,15 +307,15 @@ <string name="icon_descr_profile_image_placeholder">Platzhalter für Profilbild</string> <string name="image_descr_profile_image">Profilbild</string> <!-- Content Descriptions --> - <string name="icon_descr_close_button">Schließen der Schaltfläche</string> + <string name="icon_descr_close_button">Schließen-Button</string> <string name="image_descr_link_preview">Vorschaubild verlinken</string> - <string name="icon_descr_cancel_link_preview">Link Vorschau abbrechen</string> + <string name="icon_descr_cancel_link_preview">Linkvorschau abbrechen</string> <string name="icon_descr_settings">Einstellungen</string> - <string name="image_descr_qr_code">QR Code</string> - <string name="icon_descr_address">SimpleX Adresse</string> + <string name="image_descr_qr_code">QR-Code</string> + <string name="icon_descr_address">SimpleX-Adresse</string> <string name="icon_descr_help">Hilfe</string> - <string name="icon_descr_simplex_team">SimpleX Team</string> - <string name="image_descr_simplex_logo">SimpleX Logo</string> + <string name="icon_descr_simplex_team">SimpleX-Team</string> + <string name="image_descr_simplex_logo">SimpleX-Logo</string> <string name="icon_descr_email">E-Mail</string> <string name="icon_descr_more_button">Mehr</string> <!-- Connection info - ContactConnectionInfoView.kt --> @@ -333,28 +334,26 @@ \nan Ihren Kontakt gesendet</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Wenn Sie sich nicht persönlich treffen können, können Sie <b>den QR-Code während eines Videoanrufs scannen</b> oder Ihr Kontakt kann einen Einladungslink über einen anderen Kanal mit Ihnen teilen.]]></string> <string name="share_invitation_link">Einmal-Link teilen</string> - <string name="paste_connection_link_below_to_connect">Fügen Sie den erhaltenen Link in das Feld unten ein, um sich mit Ihrem Kontakt zu verbinden.</string> - <string name="your_profile_will_be_sent">Ihr Chat-Profil wird an Ihren Kontakt gesendet</string> <!-- PasteToConnect.kt --> <string name="connect_via_link">Über einen Link verbinden</string> <string name="connect_button">Verbinden</string> <string name="paste_button">Einfügen</string> <string name="this_string_is_not_a_connection_link">Diese Zeichenfolge entspricht keinem gültigen Verbindungslink!</string> - <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche <b>In mobiler App öffnen</b>.]]></string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Sie können sich auch verbinden, indem Sie auf den Link klicken. Wenn er im Browser geöffnet wird, klicken Sie auf die Schaltfläche „<b>In mobiler App öffnen</b>“.]]></string> <!-- CreateLinkView.kt --> - <string name="create_one_time_link">Link / QR-Code erstellen</string> - <string name="one_time_link">Einmaliger Einladungs-Link</string> + <string name="create_one_time_link">Einmal-Einladungslink erstellen</string> + <string name="one_time_link">Einmal-Einladungslink</string> <!-- settings - SettingsView.kt --> <string name="your_settings">Meine Einstellungen</string> <string name="your_simplex_contact_address">Meine SimpleX-Adresse</string> <string name="database_passphrase_and_export">Datenbank-Passwort & -Export</string> <string name="about_simplex_chat">Über SimpleX Chat</string> <string name="how_to_use_simplex_chat">Wie man SimpleX nutzt</string> - <string name="markdown_help">Markdown Hilfe</string> + <string name="markdown_help">Markdown-Hilfe</string> <string name="markdown_in_messages">Markdown in Nachrichten</string> <string name="chat_with_the_founder">Senden Sie Fragen und Ideen</string> <string name="send_us_an_email">Senden Sie uns eine E-Mail</string> - <string name="chat_lock">SimpleX Sperre</string> + <string name="chat_lock">SimpleX-Sperre</string> <string name="chat_console">Chat-Konsole</string> <string name="smp_servers">SMP-Server</string> <string name="smp_servers_preset_address">Voreingestellte Serveradresse</string> @@ -363,7 +362,7 @@ <string name="smp_servers_test_server">Teste Server</string> <string name="smp_servers_test_servers">Teste alle Server</string> <string name="smp_servers_save">Alle Server speichern</string> - <string name="smp_servers_test_failed">Server Test ist fehlgeschlagen!</string> + <string name="smp_servers_test_failed">Server-Test ist fehlgeschlagen!</string> <string name="smp_servers_test_some_failed">Einige Server haben den Test nicht bestanden:</string> <string name="smp_servers_scan_qr">Scannen Sie den QR-Code des Servers</string> <string name="smp_servers_enter_manually">Geben Sie den Server manuell ein</string> @@ -380,17 +379,17 @@ <string name="star_on_github">Stern auf GitHub vergeben</string> <string name="contribute">Unterstützen Sie uns</string> <string name="rate_the_app">Bewerten Sie die App</string> - <string name="use_simplex_chat_servers__question">Verwenden Sie SimpleX Chat Server?</string> + <string name="use_simplex_chat_servers__question">Verwenden Sie SimpleX-Chat-Server\?</string> <string name="your_SMP_servers">Ihre SMP-Server</string> - <string name="using_simplex_chat_servers">Verwendung von SimpleX Chat Servern.</string> + <string name="using_simplex_chat_servers">Verwendung von SimpleX-Chat-Servern.</string> <string name="how_to">Anleitung</string> <string name="how_to_use_your_servers">Wie Sie Ihre Server nutzen</string> - <string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC ICE-Server werden entfernt.</string> + <string name="saved_ICE_servers_will_be_removed">Gespeicherte WebRTC-ICE-Server werden entfernt.</string> <string name="your_ICE_servers">Ihre ICE-Server</string> <string name="configure_ICE_servers">ICE-Server konfigurieren</string> <string name="enter_one_ICE_server_per_line">ICE-Server (einer pro Zeile)</string> <string name="error_saving_ICE_servers">Fehler beim Speichern der ICE-Server</string> - <string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC ICE-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string> + <string name="ensure_ICE_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die WebRTC-ICE-Server-Adressen das richtige Format haben, zeilenweise separiert und nicht doppelt vorhanden sind.</string> <string name="save_servers_button">Speichern</string> <string name="network_and_servers">Netzwerk & Server</string> <string name="network_settings">Erweiterte Netzwerkeinstellungen</string> @@ -398,7 +397,7 @@ <string name="network_enable_socks">SOCKS-Proxy verwenden?</string> <string name="network_enable_socks_info">Zugriff auf die Server über SOCKS-Proxy auf Port %d? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird.</string> <string name="network_disable_socks">Direkte Internetverbindung verwenden?</string> - <string name="network_disable_socks_info">Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse und Ihren Provider sehen und mit welchen Servern Sie sich verbinden.</string> + <string name="network_disable_socks_info">Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse sowie Ihren Provider sehen und mit welchen Servern Sie sich verbinden.</string> <string name="update_onion_hosts_settings_question">Einstellung für .onion-Hosts aktualisieren?</string> <string name="network_use_onion_hosts">Verwende .onion-Hosts</string> <string name="network_use_onion_hosts_prefer">Wenn verfügbar</string> @@ -406,7 +405,8 @@ <string name="network_use_onion_hosts_required">Erforderlich</string> <string name="network_use_onion_hosts_prefer_desc">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string> <string name="network_use_onion_hosts_no_desc">Onion-Hosts werden nicht verwendet.</string> - <string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt.</string> + <string name="network_use_onion_hosts_required_desc">Für die Verbindung werden Onion-Hosts benötigt. +\nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion-Hosts werden verwendet, wenn sie verfügbar sind.</string> <string name="network_use_onion_hosts_no_desc_in_alert">Onion-Hosts werden nicht verwendet.</string> <string name="network_use_onion_hosts_required_desc_in_alert">Für die Verbindung werden Onion-Hosts benötigt.</string> @@ -432,7 +432,7 @@ <!-- Welcome Prompts - WelcomeView.kt --> <string name="you_control_your_chat">Sie haben volle Kontrolle über Ihren Chat!</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Die Messaging- und Anwendungsplattform zum Schutz Ihrer Privatsphäre und Sicherheit.</string> - <string name="we_do_not_store_contacts_or_messages_on_servers">Wir speichern auf den Servern keinen Ihrer Kontakte oder keine Ihrer Nachrichten (sofern einmal zugestellt).</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Wir speichern auf den Servern keinen Ihrer Kontakte und keine Ihrer Nachrichten (sobald einmal zugestellt).</string> <string name="create_profile">Profil erstellen</string> <string name="your_profile_is_stored_on_your_device">Ihr Profil, Ihre Kontakte und zugestellten Nachrichten werden auf Ihrem Gerät gespeichert.</string> <string name="profile_is_only_shared_with_your_contacts">Das Profil wird nur mit Ihren Kontakten geteilt.</string> @@ -471,7 +471,7 @@ <!-- SimpleXInfo --> <string name="next_generation_of_private_messaging">Die nächste Generation von privatem Messaging</string> <string name="privacy_redefined">Datenschutz neu definiert</string> - <string name="first_platform_without_user_ids">Die erste Plattform ohne Benutzerkennungen – Privat per Design</string> + <string name="first_platform_without_user_ids">Die erste Plattform ohne Benutzerkennungen – privat per Design</string> <string name="immune_to_spam_and_abuse">Immun gegen Spam und Missbrauch</string> <string name="people_can_connect_only_via_links_you_share">Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen.</string> <string name="decentralized">Dezentral</string> @@ -482,9 +482,9 @@ <!-- How SimpleX Works --> <string name="how_simplex_works">Wie SimpleX funktioniert</string> <string name="many_people_asked_how_can_it_deliver"><![CDATA[Viele Menschen haben gefragt: <i>Wie kann SimpleX Nachrichten zustellen, wenn es keine Benutzerkennungen gibt?</i>]]></string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Zum Schutz Ihrer Privatsphäre verwendet SimpleX an Stelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Zum Schutz Ihrer Privatsphäre verwendet SimpleX anstelle von Benutzerkennungen, die von allen anderen Plattformen verwendet werden, Kennungen für Nachrichtenwarteschlangen, die für jeden Ihrer Kontakte individuell sind.</string> <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Sie können selbst festlegen, über welche Server Sie Ihre Nachrichten <b>empfangen</b> und an Ihre Kontakte <b>senden</b> wollen.]]></string> - <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine <b>2-Schichten Ende-zu-Ende-Verschlüsselung</b> gesendet werden.]]></string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine <b>zweischichtige Ende-zu-Ende-Verschlüsselung</b> gesendet werden.]]></string> <string name="read_more_in_github">Erfahren Sie in unserem GitHub-Repository mehr dazu.</string> <string name="read_more_in_github_with_link"><![CDATA[Erfahren Sie in unserem <font color="#0088ff">GitHub-Repository</font> mehr dazu.]]></string> <!-- MakeConnection --> @@ -493,9 +493,9 @@ <string name="incoming_video_call">Eingehender Videoanruf</string> <string name="incoming_audio_call">Eingehender Audioanruf</string> <string name="contact_wants_to_connect_via_call">%1$s möchte sich mit Ihnen verbinden per</string> - <string name="video_call_no_encryption">Videoanruf (nicht E2E verschlüsselt)</string> + <string name="video_call_no_encryption">Videoanruf (nicht E2E-verschlüsselt)</string> <string name="encrypted_video_call">E2E-verschlüsseltem Videoanruf</string> - <string name="audio_call_no_encryption">Audioanruf (nicht E2E verschlüsselt)</string> + <string name="audio_call_no_encryption">Audioanruf (nicht E2E-verschlüsselt)</string> <string name="encrypted_audio_call">E2E-verschlüsseltem Audioanruf</string> <string name="accept">Akzeptieren</string> <string name="reject">Ablehnen</string> @@ -512,7 +512,7 @@ <string name="show_call_on_lock_screen">Anzeigen</string> <string name="no_call_on_lock_screen">Deaktivieren</string> <string name="your_ice_servers">Ihre ICE-Server</string> - <string name="webrtc_ice_servers">WebRTC ICE-Server</string> + <string name="webrtc_ice_servers">WebRTC-ICE-Server</string> <string name="relay_server_protects_ip">Relais-Server schützen Ihre IP-Adresse, aber sie können die Anrufdauer erfassen.</string> <string name="relay_server_if_necessary">Relais-Server werden nur genutzt, wenn sie benötigt werden. Ihre IP-Adresse kann von Anderen erfasst werden.</string> <!-- Call Lock Screen --> @@ -569,11 +569,11 @@ <string name="settings_developer_tools">Entwicklertools</string> <string name="settings_experimental_features">Experimentelle Funktionen</string> <string name="settings_section_title_socks">SOCKS-PROXY</string> - <string name="settings_section_title_icon">APP ICON</string> + <string name="settings_section_title_icon">APP-ICON</string> <string name="settings_section_title_themes">DESIGN</string> <string name="settings_section_title_messages">NACHRICHTEN und DATEIEN</string> <string name="settings_section_title_calls">CALLS</string> - <string name="settings_section_title_incognito">Inkognito Modus</string> + <string name="settings_section_title_incognito">Inkognito-Modus</string> <!-- DatabaseView.kt --> <string name="your_chat_database">Chat-Datenbank</string> <string name="run_chat_section">CHAT STARTEN</string> @@ -595,21 +595,21 @@ <string name="error_stopping_chat">Fehler beim Beenden des Chats</string> <string name="error_exporting_chat_database">Fehler beim Exportieren der Chat-Datenbank</string> <string name="import_database_question">Chat-Datenbank importieren?</string> - <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die Importierte ERSETZT. -\nDiese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die importierte ERSETZT. +\nDiese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string> <string name="import_database_confirmation">Importieren</string> <string name="error_deleting_database">Fehler beim Löschen der Chat-Datenbank</string> <string name="error_importing_database">Fehler beim Importieren der Chat-Datenbank</string> <string name="chat_database_imported">Chat-Datenbank importiert</string> <string name="restart_the_app_to_use_imported_chat_database">Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden.</string> <string name="delete_chat_profile_question">Chat-Profil löschen?</string> - <string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden - Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string> + <string name="delete_chat_profile_action_cannot_be_undone_warning">Diese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren.</string> <string name="chat_database_deleted">Chat-Datenbank gelöscht</string> <string name="restart_the_app_to_create_a_new_chat_profile">Starten Sie die App neu, um ein neues Chat-Profil zu erstellen.</string> <string name="you_must_use_the_most_recent_version_of_database">Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte.</string> <string name="stop_chat_to_enable_database_actions">Chat beenden, um Datenbankaktionen zu erlauben.</string> <string name="delete_files_and_media_question">Dateien und Medien löschen?</string> - <string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string> + <string name="delete_files_and_media_desc">Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten.</string> <string name="no_received_app_files">Keine empfangenen oder gesendeten Dateien</string> <string name="total_files_count_and_size">%d Datei(en) mit einem Gesamtspeicherverbrauch von %s</string> <string name="chat_item_ttl_none">nie</string> @@ -619,7 +619,7 @@ <string name="chat_item_ttl_seconds">%s Sekunde(n)</string> <string name="delete_messages_after">Löschen der Nachrichten</string> <string name="enable_automatic_deletion_question">Automatisches Löschen von Nachrichten aktivieren?</string> - <string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden - Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string> + <string name="enable_automatic_deletion_message">Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern.</string> <string name="delete_messages">Nachrichten löschen</string> <string name="error_changing_message_deletion">Fehler beim Ändern der Einstellung</string> <!-- DatabaseEncryptionView.kt --> @@ -637,10 +637,10 @@ <string name="update_database_passphrase">Datenbank-Passwort aktualisieren</string> <string name="enter_correct_current_passphrase">Bitte geben Sie das korrekte, aktuelle Passwort ein.</string> <string name="database_is_not_encrypted">Ihre Chat-Datenbank ist nicht verschlüsselt. Bitte legen Sie ein Passwort fest, um sie zu schützen.</string> - <string name="keychain_is_storing_securely">Der Android Keystore wird verwendet, um das Passwort sicher zu speichern. Dies ermöglicht die ordentliche Funktion des Benachrichtigungsdienstes.</string> + <string name="keychain_is_storing_securely">Der Android-Keystore wird verwendet, um das Passwort sicher zu speichern. Dies ermöglicht die ordentliche Funktion des Benachrichtigungsdienstes.</string> <string name="encrypted_with_random_passphrase">Die Datenbank wird mit einem zufälligen Passwort verschlüsselt, Sie können es ändern.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Bitte beachten Sie</b>: Sie können das Passwort NICHT wiederherstellen oder ändern, wenn Sie es vergessen haben oder verlieren.]]></string> - <string name="keychain_allows_to_receive_ntfs">Der Android Keystore wird verwendet, um das Passwort sicher zu speichern, nachdem Sie die App neu gestartet oder das Passwort geändert haben – dies ermöglicht den Empfang von Benachrichtigungen.</string> + <string name="keychain_allows_to_receive_ntfs">Der Android-Keystore wird verwendet, um das Passwort sicher zu speichern, nachdem Sie die App neu gestartet oder das Passwort geändert haben – dies ermöglicht den Empfang von Benachrichtigungen.</string> <string name="you_have_to_enter_passphrase_every_time">Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert.</string> <string name="encrypt_database_question">Datenbank verschlüsseln?</string> <string name="change_database_passphrase_question">Datenbank-Passwort ändern?</string> @@ -654,7 +654,7 @@ <string name="wrong_passphrase">Falsches Datenbank-Passwort</string> <string name="encrypted_database">Verschlüsselte Datenbank</string> <string name="database_error">Datenbankfehler</string> - <string name="keychain_error">Keystore Fehler</string> + <string name="keychain_error">Keystore-Fehler</string> <string name="passphrase_is_different">Das Datenbank-Passwort unterscheidet sich von dem im Keystore gespeicherten.</string> <string name="file_with_path">Datei: %s</string> <string name="database_passphrase_is_required">Das Datenbank-Passwort ist erforderlich, um den Chat zu öffnen.</string> @@ -676,14 +676,14 @@ <string name="restore_passphrase_not_found_desc">Das Passwort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie es manuell ein. Das kann passieren, wenn Sie die App-Daten mit einem Backup-Programm wieder hergestellt haben. Bitte nehmen Sie Kontakt mit den Entwicklern auf, wenn das nicht der Fall ist.</string> <!-- ChatModel.chatRunning interactions --> <string name="chat_is_stopped_indication">Chat wurde beendet</string> - <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten.</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Sie können den Chat über die App-Einstellungen/Datenbank oder durch Neustart der App starten.</string> <!-- ChatArchiveView.kt --> - <string name="chat_archive_header">Datenbank Archiv</string> - <string name="chat_archive_section">CHAT ARCHIV</string> + <string name="chat_archive_header">Datenbank-Archiv</string> + <string name="chat_archive_section">CHAT-ARCHIV</string> <string name="save_archive">Archiv speichern</string> <string name="delete_archive">Archiv löschen</string> <string name="archive_created_on_ts">Erstellt am %1$s</string> - <string name="delete_chat_archive_question">Chat Archiv löschen?</string> + <string name="delete_chat_archive_question">Chat-Archiv löschen\?</string> <!-- Groups --> <string name="group_invitation_item_description">Einladung zur Gruppe %1$s</string> <string name="join_group_question">Der Gruppe beitreten?</string> @@ -698,7 +698,7 @@ <string name="icon_descr_add_members">Mitglieder einladen</string> <string name="icon_descr_group_inactive">Gruppe inaktiv</string> <string name="alert_title_group_invitation_expired">Einladung abgelaufen!</string> - <string name="alert_message_group_invitation_expired">Die Gruppeneinladung ist nicht mehr gültig, sie wurde vom Absender entfernt.</string> + <string name="alert_message_group_invitation_expired">Die Gruppeneinladung ist nicht mehr gültig, da sie vom Absender entfernt wurde.</string> <string name="alert_title_no_group">Die Gruppe wurde nicht gefunden!</string> <string name="alert_message_no_group">Diese Gruppe existiert nicht mehr.</string> <string name="alert_title_cant_invite_contacts">Kontakte können nicht eingeladen werden!</string> @@ -744,7 +744,7 @@ <string name="group_member_status_group_deleted">Gruppe gelöscht</string> <string name="group_member_status_invited">Eingeladen</string> <string name="group_member_status_introduced">Verbindung (erstellt)</string> - <string name="group_member_status_intro_invitation">Verbindung (eingeladen)</string> + <string name="group_member_status_intro_invitation">Verbinde (nach einer Einladung)</string> <string name="group_member_status_accepted">Verbindung (akzeptiert)</string> <string name="group_member_status_announced">Verbindung (angekündigt)</string> <string name="group_member_status_connected">Verbunden</string> @@ -770,19 +770,19 @@ <string name="group_info_member_you">Sie: %1$s</string> <string name="button_delete_group">Gruppe löschen</string> <string name="delete_group_question">Gruppe löschen?</string> - <string name="delete_group_for_all_members_cannot_undo_warning">Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden!</string> - <string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Die Gruppe wird für alle Mitglieder gelöscht – dies kann nicht rückgängig gemacht werden!</string> + <string name="delete_group_for_self_cannot_undo_warning">Die Gruppe wird für Sie gelöscht – dies kann nicht rückgängig gemacht werden!</string> <string name="button_leave_group">Gruppe verlassen</string> <string name="button_edit_group_profile">Gruppenprofil bearbeiten</string> <string name="group_link">Gruppen-Link</string> <string name="button_create_group_link">Link erzeugen</string> <string name="delete_link_question">Link löschen?</string> <string name="delete_link">Link löschen</string> - <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Sie können diesen Link oder QR-Code teilen - Damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Sie können diesen Link oder QR-Code teilen – damit kann jede Person der Gruppe beitreten. Wenn Sie den Link später löschen, werden Sie keine Gruppenmitglieder verlieren, die der Gruppe darüber beigetreten sind.</string> <string name="all_group_members_will_remain_connected">Alle Gruppenmitglieder bleiben verbunden.</string> <string name="error_creating_link_for_group">Fehler beim Erzeugen des Gruppen-Links</string> <string name="error_deleting_link_for_group">Fehler beim Löschen des Gruppen-Links</string> - <string name="only_group_owners_can_change_prefs">Gruppenpräferenzen können nur von Gruppen-Eigentümern geändert werden.</string> + <string name="only_group_owners_can_change_prefs">Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden.</string> <!-- For Console chat info section --> <string name="section_title_for_console">FÜR KONSOLE</string> <string name="info_row_local_name">Lokaler Name</string> @@ -790,7 +790,7 @@ <!-- GroupMemberInfoView.kt --> <string name="button_remove_member">Mitglied entfernen</string> <string name="button_send_direct_message">Direktnachricht senden</string> - <string name="member_will_be_removed_from_group_cannot_be_undone">Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Das Mitglied wird aus der Gruppe entfernt – dies kann nicht rückgängig gemacht werden!</string> <string name="remove_member_confirmation">Entfernen</string> <string name="member_info_section_title_member">MITGLIED</string> <string name="role_in_group">Rolle</string> @@ -817,7 +817,6 @@ <string name="group_is_decentralized">Die Gruppe ist vollständig dezentralisiert – sie ist nur für Mitglieder sichtbar.</string> <string name="group_display_name_field">Anzeigename der Gruppe:</string> <string name="group_full_name_field">Vollständiger Gruppenname:</string> - <string name="group_unsupported_incognito_main_profile_sent">Der Inkognito-Modus wird hier nicht unterstützt - Ihr Hauptprofil wird an die Gruppenmitglieder gesendet</string> <string name="group_main_profile_sent">Ihr Chat-Profil wird an die Gruppenmitglieder gesendet</string> <!-- GroupProfileView.kt --> <string name="group_profile_is_stored_on_members_devices">Das Gruppenprofil wird auf den Geräten der Mitglieder gespeichert, nicht auf den Servern.</string> @@ -829,7 +828,7 @@ <string name="network_option_tcp_connection_timeout">Timeout der TCP-Verbindung</string> <string name="network_option_protocol_timeout">Protokollzeitüberschreitung</string> <string name="network_option_ping_interval">PING-Intervall</string> - <string name="network_option_enable_tcp_keep_alive">TCP-Keep-alive aktivieren</string> + <string name="network_option_enable_tcp_keep_alive">TCP-Keep-Alive aktivieren</string> <string name="network_options_revert">Zurückkehren</string> <string name="network_options_save">Speichern</string> <string name="update_network_settings_question">Netzwerkeinstellungen aktualisieren?</string> @@ -838,12 +837,9 @@ <!-- Incognito mode --> <string name="incognito">Inkognito</string> <string name="incognito_random_profile">Ihr Zufallsprofil</string> - <string name="incognito_random_profile_description">Ein zufälliges Profil wird an Ihren Kontakt gesendet.</string> - <string name="incognito_random_profile_from_contact_description">Ein zufälliges Profil wird an den Kontakt gesendet, von dem Sie diesen Link erhalten haben.</string> - <string name="incognito_info_protects">Der Inkognito-Modus schützt die Privatsphäre Ihres Hauptprofilnamens und -bildes – für jeden neuen Kontakt wird ein neues Zufallsprofil erstellt.</string> - <string name="incognito_info_allows">Es ermöglicht viele anonyme Verbindungen ohne gemeinsame Daten zwischen diesen in einem einzigen Chat-Profil zu haben.</string> - <string name="incognito_info_share">Wenn Sie ein Inkognito-Profil mit Jemandem teilen, wird dieses Profil für die Gruppen verwendet, zu denen sie Sie eingeladen haben.</string> - <string name="incognito_info_find">Um das für eine Inkognito-Verbindung verwendete Profil zu finden, tippen Sie oben im Chat auf den Kontakt- oder Gruppennamen.</string> + <string name="incognito_info_protects">Der Inkognito-Modus schützt Ihre Privatsphäre, indem für jeden Kontakt ein neues Zufallsprofil erstellt wird.</string> + <string name="incognito_info_allows">Es ermöglicht, viele anonyme Verbindungen ohne gemeinsame Daten zwischen diesen in einem einzigen Chat-Profil zu haben.</string> + <string name="incognito_info_share">Wenn Sie ein Inkognito-Profil mit jemandem teilen, wird dieses Profil für die Gruppen verwendet, zu denen derjenige Sie einlädt.</string> <!-- Default themes --> <string name="theme_system">System</string> <string name="theme_light">Hell</string> @@ -862,37 +858,37 @@ <string name="chat_preferences_always">Immer</string> <string name="chat_preferences_on">Ein</string> <string name="chat_preferences_off">Aus</string> - <string name="chat_preferences">Chat Präferenzen</string> - <string name="contact_preferences">Kontakt Präferenzen</string> - <string name="group_preferences">Gruppen Präferenzen</string> - <string name="set_group_preferences">Gruppenpräferenzen einstellen</string> + <string name="chat_preferences">Chat-Präferenzen</string> + <string name="contact_preferences">Kontakt-Präferenzen</string> + <string name="group_preferences">Gruppen-Präferenzen</string> + <string name="set_group_preferences">Gruppen-Präferenzen einstellen</string> <string name="your_preferences">Ihre Präferenzen</string> <string name="direct_messages">Direkte Nachrichten</string> - <string name="full_deletion">Für Alle löschen</string> + <string name="full_deletion">Für jeden löschen</string> <string name="voice_messages">Sprachnachrichten</string> <string name="feature_enabled">aktiviert</string> <string name="feature_enabled_for_you">Für Sie aktiviert</string> - <string name="feature_enabled_for_contact">FÜr Kontakt aktiviert</string> + <string name="feature_enabled_for_contact">Für Kontakt aktiviert</string> <string name="feature_off">Aus</string> <string name="feature_received_prohibited">empfangen, nicht erlaubt</string> <string name="allow_your_contacts_irreversibly_delete">Erlauben Sie Ihren Kontakten gesendete Nachrichten unwiederbringlich zu löschen.</string> - <string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</string> + <string name="allow_irreversible_message_deletion_only_if">Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> <string name="contacts_can_mark_messages_for_deletion">Ihre Kontakte können Nachrichten zum Löschen markieren. Sie können diese Nachrichten trotzdem anschauen.</string> <string name="allow_your_contacts_to_send_voice_messages">Erlauben Sie Ihre Kontakten Sprachnachrichten zu versenden.</string> - <string name="allow_voice_messages_only_if">Erlauben Sie Sprachnachrichten nur dann, wenn Ihr Kontakt diese ebenfalls erlaubt.</string> + <string name="allow_voice_messages_only_if">Erlauben Sie Sprachnachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> <string name="prohibit_sending_voice_messages">Das Senden von Sprachnachrichten nicht erlauben.</string> - <string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt, als auch Sie können Nachrichten unwiederbringlich löschen.</string> + <string name="both_you_and_your_contacts_can_delete">Sowohl Ihr Kontakt als auch Sie können Nachrichten unwiederbringlich löschen.</string> <string name="only_you_can_delete_messages">Nur Sie können Nachrichten unwiederbringlich löschen (Ihr Kontakt kann sie zum Löschen markieren).</string> <string name="only_your_contact_can_delete">Nur Ihr Kontakt kann Nachrichten unwiederbringlich löschen (Sie können sie zum Löschen markieren).</string> <string name="message_deletion_prohibited">In dieser Gruppe ist das unwiederbringliche Löschen von Nachrichten nicht erlaubt.</string> - <string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt, als auch Sie können Sprachnachrichten versenden.</string> + <string name="both_you_and_your_contact_can_send_voice">Sowohl Ihr Kontakt als auch Sie können Sprachnachrichten versenden.</string> <string name="only_you_can_send_voice">Nur Sie können Sprachnachrichten versenden.</string> <string name="only_your_contact_can_send_voice">Nur Ihr Kontakt kann Sprachnachrichten versenden.</string> <string name="voice_prohibited_in_this_chat">In diesem Chat sind Sprachnachrichten nicht erlaubt.</string> <string name="allow_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder erlauben.</string> <string name="prohibit_direct_messages">Das Senden von Direktnachrichten an Gruppenmitglieder nicht erlauben.</string> <string name="allow_to_delete_messages">Unwiederbringliches löschen von gesendeten Nachrichten erlauben.</string> - <string name="prohibit_message_deletion">Unwiederbringliches löschen von Nachrichten nicht erlauben.</string> + <string name="prohibit_message_deletion">Unwiederbringliches Löschen von Nachrichten nicht erlauben.</string> <string name="allow_to_send_voice">Das Senden von Sprachnachrichten erlauben.</string> <string name="prohibit_sending_voice">Das Senden von Sprachnachrichten nicht erlauben.</string> <string name="group_members_can_send_dms">Gruppenmitglieder können Direktnachrichten versenden.</string> @@ -905,10 +901,10 @@ <string name="view_security_code">Schauen Sie sich den Sicherheitscode an</string> <string name="onboarding_notifications_mode_service">Sofort</string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Gute Option für die Batterieausdauer</b>. Der Hintergrundservice überprüft alle 10 Minuten nach Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]></string> - <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Beste Option für die Batterieausdauer</b>. Sie empfangen Benachrichtigungen nur solange die App abläuft (kein aktiver Hintergrundservice).]]></string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Beste Option für die Akkulaufzeit</b>. Sie empfangen Benachrichtigungen nur, solange die App läuft (kein aktiver Hintergrundservice).]]></string> <string name="send_verb">Senden</string> <string name="is_verified">%s wurde erfolgreich überprüft</string> - <string name="clear_verification">Überprüfung zurücknehmen</string> + <string name="clear_verification">Verifikation zurücknehmen</string> <string name="onboarding_notifications_mode_off">Solange die App abläuft</string> <string name="onboarding_notifications_mode_subtitle">Kann später über die Einstellungen geändert werden.</string> <string name="delete_after">Löschen nach</string> @@ -947,14 +943,14 @@ <string name="prohibit_sending_disappearing_messages">Das Senden von verschwindenden Nachrichten verbieten.</string> <string name="disappearing_messages_are_prohibited">In dieser Gruppe sind verschwindende Nachrichten nicht erlaubt.</string> <string name="group_members_can_send_disappearing">Gruppenmitglieder können verschwindende Nachrichten senden.</string> - <string name="v4_3_improved_server_configuration_desc">Fügen Sie Server durch Scannen der QR Codes hinzu.</string> + <string name="v4_3_improved_server_configuration_desc">Fügen Sie Server durch Scannen der QR-Codes hinzu.</string> <string name="v4_4_disappearing_messages">Verschwindende Nachrichten</string> <string name="accept_feature">Übernehmen</string> <string name="accept_feature_set_1_day">Einen Tag festlegen</string> <string name="invalid_chat">Ungültiger Chat</string> - <string name="live_message">Live Nachricht!</string> - <string name="send_live_message_desc">Eine Live Nachricht senden - der/die Empfänger sieht/sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string> - <string name="send_live_message">Live Nachricht senden</string> + <string name="live_message">Live-Nachricht!</string> + <string name="send_live_message_desc">Eine Live-Nachricht senden – der/die Empfänger sieht/sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string> + <string name="send_live_message">Live-Nachricht senden</string> <string name="verify_security_code">Sicherheitscode überprüfen</string> <string name="is_not_verified">%s wurde noch nicht überprüft</string> <string name="to_verify_compare">Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen.</string> @@ -974,13 +970,13 @@ <string name="v4_3_improved_privacy_and_security_desc">App-Bildschirm in aktuellen Anwendungen verbergen.</string> <string name="v4_3_improved_privacy_and_security">Verbesserte Privatsphäre und Sicherheit</string> <string name="v4_3_improved_server_configuration">Verbesserte Serverkonfiguration</string> - <string name="v4_3_irreversible_message_deletion">Unwiederbringliches löschen einer Nachricht</string> - <string name="v4_4_live_messages">Live Nachrichten</string> + <string name="v4_3_irreversible_message_deletion">Unwiederbringliches Löschen einer Nachricht</string> + <string name="v4_4_live_messages">Live-Nachrichten</string> <string name="v4_3_voice_messages_desc">Max. 40 Sekunden, sofort erhalten.</string> <string name="v4_4_live_messages_desc">Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben.</string> <string name="v4_4_disappearing_messages_desc">Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht.</string> <string name="v4_3_voice_messages">Sprachnachrichten</string> - <string name="allow_disappearing_messages_only_if">Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt.</string> + <string name="allow_disappearing_messages_only_if">Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> <string name="invalid_data">Ungültige Daten</string> <string name="v4_4_verify_connection_security">Sicherheit der Verbindung überprüfen</string> <string name="v4_2_auto_accept_contact_requests_desc">Mit optionaler Begrüßungsmeldung.</string> @@ -993,13 +989,13 @@ <string name="delete_files_and_media_all">Alle Dateien löschen</string> <string name="messages_section_title">Nachrichten</string> <string name="app_version_code">App Build: %s</string> - <string name="app_version_title">App Version</string> - <string name="app_version_name">App Version: v%s</string> + <string name="app_version_title">App-Version</string> + <string name="app_version_name">App-Version: v%s</string> <string name="core_version">Core Version: v%s</string> <string name="users_add">Profil hinzufügen</string> <string name="users_delete_all_chats_deleted">Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!</string> <string name="users_delete_profile_for">Chat-Profil löschen für</string> - <string name="network_option_ping_count">PING Zähler</string> + <string name="network_option_ping_count">PING-Zähler</string> <string name="update_network_session_mode_question">Transport-Isolations-Modus aktualisieren\?</string> <string name="smp_servers_per_user">Server der neuen Verbindungen von Ihrem aktuellen Chat-Profil</string> <string name="files_and_media_section">Dateien & Medien</string> @@ -1012,7 +1008,7 @@ <string name="delete_files_and_media_for_all_users">Dateien für alle Chat-Profile löschen</string> <string name="network_session_mode_entity_description"><b>Für jeden Kontakt und jedes Gruppenmitglied</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt. \n -\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindung haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern.</string> +\n<b>Bitte beachten Sie</b>: Wenn Sie viele Verbindungen haben, können Akkuverbrauch und Datennutzung wesentlich höher ausfallen und einige Verbindungen scheitern.</string> <string name="network_session_mode_user_description"><![CDATA[<b>Für jedes von Ihnen in der App genutzte Chat-Profil</b> wird eine separate TCP-Verbindung (und SOCKS-Berechtigung) genutzt.]]></string> <string name="users_delete_data_only">Nur lokale Profildaten</string> <string name="users_delete_with_connections">Profil und Serververbindungen</string> @@ -1022,7 +1018,7 @@ <string name="failed_to_active_user_title">Fehler beim Umschalten des Profils!</string> <string name="failed_to_create_user_duplicate_desc">Sie haben schon ein Chat-Profil mit dem gleichen Anzeigenamen. Bitte wählen Sie einen anderen Namen aus.</string> <string name="v4_5_multiple_chat_profiles">Mehrere Chat-Profile</string> - <string name="v4_5_reduced_battery_usage">Reduzierter Batterieverbrauch</string> + <string name="v4_5_reduced_battery_usage">Reduzierter Akkuverbrauch</string> <string name="v4_5_private_filenames">Neutrale Dateinamen</string> <string name="v4_5_message_draft_descr">Den letzten Nachrichtenentwurf, auch mit seinen Anhängen, aufbewahren.</string> <string name="v4_5_transport_isolation_descr">Per Chat-Profil (Voreinstellung) oder per Verbindung (BETA).</string> @@ -1067,7 +1063,7 @@ <string name="enter_password_to_show">Für die Anzeige das Passwort im Suchfeld eingeben</string> <string name="make_profile_private">Privates Profil erzeugen!</string> <string name="user_mute">Stummschalten</string> - <string name="tap_to_activate_profile">Tippen Sie auf das Profil um es zu aktivieren.</string> + <string name="tap_to_activate_profile">Tippen Sie auf das Profil, um es zu aktivieren.</string> <string name="should_be_at_least_one_profile">Es muss mindestens ein Benutzer-Profil vorhanden sein.</string> <string name="should_be_at_least_one_visible_profile">Es muss mindestens ein sichtbares Benutzer-Profil vorhanden sein.</string> <string name="user_unmute">Stummschaltung aufheben</string> @@ -1076,7 +1072,7 @@ <string name="v4_6_audio_video_calls_descr">Bluetooth-Unterstützung und weitere Verbesserungen.</string> <string name="v4_6_group_moderation_descr">Administratoren können nun \n- Nachrichten von Gruppenmitgliedern löschen -\n- Gruppenmitglieder deaktivieren (\"Beobachter\"-Rolle)</string> +\n- Gruppenmitglieder deaktivieren („Beobachter“-Rolle)</string> <string name="v4_6_group_welcome_message">Gruppen-Begrüßungsmeldung</string> <string name="v4_6_reduced_battery_usage">Weiter reduzierter Batterieverbrauch</string> <string name="v4_6_reduced_battery_usage_descr">Weitere Verbesserungen sind bald verfügbar!</string> @@ -1088,7 +1084,7 @@ <string name="save_and_update_group_profile">Gruppen-Profil sichern und aktualisieren</string> <string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string> <string name="group_welcome_title">Begrüßungsmeldung</string> - <string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten - für das Menü gedrückt halten.</string> + <string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten – für das Menü gedrückt halten.</string> <string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Meine Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string> <string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string> <string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string> @@ -1126,11 +1122,11 @@ <string name="host_verb">Host</string> <string name="error_saving_xftp_servers">Fehler beim Speichern der XFTP-Server</string> <string name="error_loading_smp_servers">Fehler beim Laden der SMP-Server</string> - <string name="error_xftp_test_server_auth">Bitte das Passwort überprüfen - für den Upload benötigt der Server eine Berechtigung</string> + <string name="error_xftp_test_server_auth">Bitte das Passwort überprüfen – für den Upload benötigt der Server eine Berechtigung</string> <string name="smp_server_test_download_file">Datei herunterladen</string> <string name="smp_server_test_compare_file">Datei vergleichen</string> <string name="smp_server_test_delete_file">Datei löschen</string> - <string name="lock_mode">Sperr-Modus</string> + <string name="lock_mode">Sperrmodus</string> <string name="authentication_cancelled">Authentifizierung abgebrochen</string> <string name="confirm_passcode">Zugangscode bestätigen</string> <string name="enable_lock">Sperre aktivieren</string> @@ -1140,7 +1136,7 @@ <string name="la_mode_passcode">Zugangscode</string> <string name="passcode_changed">Zugangscode wurde geändert!</string> <string name="passcode_set">Zugangscode wurde eingestellt!</string> - <string name="change_lock_mode">Sperr-Modus ändern</string> + <string name="change_lock_mode">Sperrmodus ändern</string> <string name="passcode_not_changed">Zugangscode wurde nicht geändert!</string> <string name="decryption_error">Entschlüsselungsfehler</string> <string name="error_loading_xftp_servers">Fehler beim Laden der XFTP-Server</string> @@ -1156,38 +1152,38 @@ <string name="smp_server_test_create_file">Datei erstellen</string> <string name="la_current_app_passcode">Aktueller Zugangscode</string> <string name="alert_text_fragment_encryption_out_of_sync_old_database">Dies kann passieren, falls Sie oder Ihre Verbindung ein altes Datenbank-Backup genutzt haben.</string> - <string name="la_please_remember_to_store_password">Es gibt keine Möglichkeit ein vergessenes Passwort wiederherzustellen - bitte erinnern Sie sich gut daran oder speichern Sie es sicher ab!</string> + <string name="la_please_remember_to_store_password">Es gibt keine Möglichkeit, ein vergessenes Passwort wiederherzustellen - bitte erinnern Sie sich gut daran oder speichern Sie es sicher ab!</string> <string name="alert_text_fragment_please_report_to_developers">Bitte melden Sie es den Entwicklern.</string> <string name="la_change_app_passcode">Zugangscode ändern</string> <string name="alert_title_msg_bad_hash">Ungültiger Nachrichten-Hash</string> <string name="la_auth_failed">Authentifizierung fehlgeschlagen</string> <string name="alert_title_msg_bad_id">Falsche Nachrichten-ID</string> - <string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server Adressen das richtige Format haben, zeilenweise angeordnet und nicht doppelt vorhanden sind.</string> + <string name="ensure_xftp_server_address_are_correct_format_and_unique">Stellen Sie sicher, dass die XFTP-Server-Adressen das richtige Format haben, zeilenweise separiert und nicht doppelt vorhanden sind.</string> <string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string> - <string name="la_lock_mode">SimpleX Sperr-Modus</string> - <string name="lock_not_enabled">SimpleX Sperre ist nicht aktiviert!</string> + <string name="la_lock_mode">SimpleX-Sperrmodus</string> + <string name="lock_not_enabled">SimpleX-Sperre ist nicht aktiviert!</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string> <string name="submit_passcode">Bestätigen</string> <string name="la_mode_system">System</string> - <string name="la_could_not_be_verified">Sie können nicht überprüft werden - bitte versuchen Sie es nochmal.</string> + <string name="la_could_not_be_verified">Sie können nicht überprüft werden – bitte versuchen Sie es nochmal.</string> <string name="xftp_servers">XFTP-Server</string> - <string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der Vorherigen). + <string name="alert_text_msg_bad_id">Die ID der nächsten Nachricht ist falsch (kleiner oder gleich der vorherigen). \nDies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompromittiert wurde.</string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d Nachrichten konnten nicht entschlüsselt werden.</string> <string name="alert_text_msg_bad_hash">Der Hash der vorherigen Nachricht unterscheidet sich.</string> - <string name="you_can_turn_on_lock">Sie können die SimpleX Sperre über die Einstellungen aktivieren.</string> - <string name="network_socks_proxy_settings">SOCKS-Proxy Einstellungen</string> + <string name="you_can_turn_on_lock">Sie können die SimpleX-Sperre über die Einstellungen aktivieren.</string> + <string name="network_socks_proxy_settings">SOCKS-Proxy-Einstellungen</string> <string name="la_lock_mode_system">System-Authentifizierung</string> <string name="smp_server_test_upload_file">Datei hochladen</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d Nachrichten übersprungen.</string> - <string name="allow_calls_only_if">Anrufe sind nur erlaubt, wenn Ihr Kontakt das ebenfalls erlaubt.</string> - <string name="allow_your_contacts_to_call">Erlaubt Ihren Kontakten Sie anzurufen.</string> - <string name="audio_video_calls">Audio/Video Anrufe</string> + <string name="allow_calls_only_if">Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> + <string name="allow_your_contacts_to_call">Erlaubt es Ihren Kontakten Sie anzurufen.</string> + <string name="audio_video_calls">Audio-/Video-Anrufe</string> <string name="available_in_v51">" \nVerfügbar in v5.1"</string> - <string name="both_you_and_your_contact_can_make_calls">Sowohl Sie, als auch Ihr Kontakt können Anrufe tätigen.</string> + <string name="both_you_and_your_contact_can_make_calls">Sowohl Sie als auch Ihr Kontakt können Anrufe tätigen.</string> <string name="only_you_can_make_calls">Nur Sie können Anrufe tätigen.</string> - <string name="prohibit_calls">Audio/Video Anrufe nicht erlauben.</string> + <string name="prohibit_calls">Audio-/Video-Anrufe nicht erlauben.</string> <string name="stop_rcv_file__title">Den Empfang der Datei beenden\?</string> <string name="stop_snd_file__title">Das Senden der Datei beenden\?</string> <string name="stop_rcv_file__message">Der Empfang der Datei wird beendet.</string> @@ -1200,13 +1196,13 @@ <string name="revoke_file__title">Datei widerrufen\?</string> <string name="stop_file__confirm">Beenden</string> <string name="v5_0_large_files_support">Videos und Dateien bis zu 1GB</string> - <string name="v5_0_large_files_support_descr">Schnell und ohne warten auf den Absender, bis er online ist!</string> + <string name="v5_0_large_files_support_descr">Schnell und ohne zu warten, bis der Kontakt online ist!</string> <string name="v5_0_polish_interface">Polnische Bedienoberfläche</string> <string name="v5_0_app_passcode_descr">Anstelle der System-Authentifizierung festlegen.</string> <string name="v5_0_polish_interface_descr">Dank der Nutzer - Tragen Sie per Weblate bei!</string> <string name="only_your_contact_can_make_calls">Nur Ihr Kontakt kann Anrufe tätigen.</string> <string name="v5_0_app_passcode">App-Zugangscode</string> - <string name="calls_prohibited_with_this_contact">Audio/Video Anrufe sind nicht erlaubt.</string> + <string name="calls_prohibited_with_this_contact">Audio-/Video-Anrufe sind nicht erlaubt.</string> <string name="address_section_title">Adresse</string> <string name="share_address">Adresse teilen</string> <string name="export_theme">Design exportieren</string> @@ -1230,9 +1226,9 @@ <string name="auto_accept_contact">Automatisch akzeptieren</string> <string name="enter_welcome_message_optional">Geben Sie eine Begrüßungsmeldung ein … (optional)</string> <string name="invite_friends">Freunde einladen</string> - <string name="email_invite_subject">Lassen Sie uns in SimpleX Chat kommunizieren</string> + <string name="email_invite_subject">Lassen Sie uns über SimpleX Chat schreiben</string> <string name="profile_update_will_be_sent_to_contacts">Profil-Aktualisierung wird an Ihre Kontakte gesendet.</string> - <string name="save_auto_accept_settings">Einstellungen von \"Automatisch akzeptieren\" speichern</string> + <string name="save_auto_accept_settings">Einstellungen von „automatisch akzeptieren“ speichern</string> <string name="save_settings_question">Einstellungen speichern\?</string> <string name="share_address_with_contacts_question">Die Adresse mit Kontakten teilen\?</string> <string name="stop_sharing">Teilen beenden</string> @@ -1259,9 +1255,9 @@ <string name="dark_theme">Dunkles Design</string> <string name="if_you_cant_meet_in_person">Falls Sie sich nicht persönlich treffen können, zeigen Sie den QR-Code in einem Videoanruf oder teilen Sie den Link.</string> <string name="read_more_in_user_guide_with_link"><![CDATA[Lesen Sie mehr dazu in der <font color="#0088ff">Benutzeranleitung</font>.]]></string> - <string name="import_theme_error_desc">Stellen Sie sicher, dass die Datei die korrekte YAML-Syntax hat. Exportieren sie das Design, um ein Beispiel für die Dateistruktur des Designs zu erhalten.</string> + <string name="import_theme_error_desc">Stellen Sie sicher, dass die Datei die korrekte YAML-Syntax hat. Exportieren Sie das Design, um ein Beispiel für die Dateistruktur des Designs zu erhalten.</string> <string name="auth_open_chat_profiles">Offene Chat-Profile</string> - <string name="you_can_share_your_address">Sie können Ihre Adresse als Link oder QR-Code teilen - jede Person kann sich mit Ihnen verbinden.</string> + <string name="you_can_share_your_address">Sie können Ihre Adresse als Link oder QR-Code teilen – jede Person kann sich mit Ihnen verbinden.</string> <string name="your_contacts_will_see_it">Ihre Kontakte in SimpleX werden es sehen. \nSie können es in den Einstellungen ändern.</string> <string name="all_app_data_will_be_cleared">Werden die App-Daten komplett gelöscht.</string> @@ -1302,11 +1298,11 @@ <string name="current_version_timestamp">%s (aktuell)</string> <string name="share_text_sent_at">Gesendet um: %s</string> <string name="message_reactions">Reaktionen auf Nachrichten</string> - <string name="allow_your_contacts_adding_message_reactions">Erlauben Sie Ihren Kontakten Reaktionen auf Nachrichten zu geben.</string> - <string name="both_you_and_your_contact_can_add_message_reactions">Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben.</string> + <string name="allow_your_contacts_adding_message_reactions">Erlauben Sie Ihren Kontakten, Reaktionen auf Nachrichten zu geben.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Sowohl Sie als auch Ihr Kontakt können Reaktionen auf Nachrichten geben.</string> <string name="only_you_can_add_message_reactions">Nur Sie können Reaktionen auf Nachrichten geben.</string> <string name="prohibit_message_reactions">Reaktionen auf Nachrichten nicht erlauben.</string> - <string name="allow_message_reactions_only_if">Reaktionen auf Nachrichten sind nur möglich, falls Ihr Kontakt dies erlaubt.</string> + <string name="allow_message_reactions_only_if">Erlauben Sie Reaktionen auf Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt.</string> <string name="only_your_contact_can_add_message_reactions">Nur Ihr Kontakt kann Reaktionen auf Nachrichten geben.</string> <string name="allow_message_reactions">Reaktionen auf Nachrichten erlauben.</string> <string name="prohibit_message_reactions_group">Reaktionen auf Nachrichten nicht erlauben.</string> @@ -1317,7 +1313,7 @@ <string name="v5_1_self_destruct_passcode">Selbstzerstörungs-Zugangscode</string> <string name="v5_1_self_destruct_passcode_descr">Alle Daten werden gelöscht, sobald dieser eingegeben wird.</string> <string name="v5_1_custom_themes">Benutzerdefinierte Designs</string> - <string name="v5_1_japanese_portuguese_interface">Japanische und Portugiesische Bedienoberfläche</string> + <string name="v5_1_japanese_portuguese_interface">Japanische und portugiesische Bedienoberfläche</string> <string name="custom_time_unit_minutes">Minuten</string> <string name="custom_time_unit_seconds">Sekunden</string> <string name="whats_new_thanks_to_users_contribute_weblate">Dank der Nutzer - Tragen Sie per Weblate bei!</string> @@ -1341,7 +1337,7 @@ <string name="edit_history">Vergangenheit</string> <string name="message_reactions_prohibited_in_this_chat">In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt.</string> <string name="item_info_no_text">Kein Text</string> - <string name="non_fatal_errors_occured_during_import">Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - weitere Details finden Sie in der Chat-Konsole.</string> + <string name="non_fatal_errors_occured_during_import">Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten – weitere Details finden Sie in der Chat-Konsole.</string> <string name="shutdown_alert_question">Herunterfahren\?</string> <string name="shutdown_alert_desc">Bis zum Neustart der App erhalten Sie keine Benachrichtigungen mehr</string> <string name="settings_section_title_app">APP</string> @@ -1363,7 +1359,7 @@ <string name="favorite_chat">Favorit</string> <string name="no_filtered_chats">Keine gefilterten Chats</string> <string name="la_mode_off">Aus</string> - <string name="network_option_protocol_timeout_per_kb">Protokollzeitüberschreitung pro kB</string> + <string name="network_option_protocol_timeout_per_kb">Protokollzeitüberschreitung pro KB</string> <string name="conn_event_ratchet_sync_started">Verschlüsselung zustimmen…</string> <string name="snd_conn_event_ratchet_sync_started">Verschlüsselung von %s zustimmen</string> <string name="conn_event_ratchet_sync_allowed">Neuaushandlung der Verschlüsselung erlaubt</string> @@ -1390,8 +1386,8 @@ <string name="fix_connection">Verbindung reparieren</string> <string name="delivery_receipts_are_disabled">Empfangsbestätigungen sind deaktiviert!</string> <string name="delivery_receipts_title">Empfangsbestätigungen!</string> - <string name="error_enabling_delivery_receipts">Fehler beim Aktivieren der Empfangsbestätigungen!</string> - <string name="receipts_contacts_disable_for_all">Für Alle deaktivieren</string> + <string name="error_enabling_delivery_receipts">Fehler beim Aktivieren von Empfangsbestätigungen!</string> + <string name="receipts_contacts_disable_for_all">Für alle deaktivieren</string> <string name="enable_receipts_all">Aktivieren</string> <string name="v5_2_favourites_filter">Chats schneller finden</string> <string name="v5_2_disappear_one_message">Eine verschwindende Nachricht verfassen</string> @@ -1399,21 +1395,21 @@ <string name="receipts_contacts_enable_keep_overrides">Aktivieren (vorgenommene Einstellungen bleiben erhalten)</string> <string name="v5_2_favourites_filter_descr">Nach ungelesenen und favorisierten Chats filtern.</string> <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert.</string> - <string name="receipts_contacts_override_disabled">Das Senden von Empfangsbestätigungen an %d Kontakte ist deaktiviert</string> + <string name="receipts_contacts_override_disabled">Das Senden von Bestätigungen an %d Kontakte ist deaktiviert</string> <string name="receipts_section_description">Diese Einstellungen gelten für Ihr aktuelles Profil</string> - <string name="receipts_section_description_1">Diese können in den Kontakt-Einstellungen überschrieben werden</string> + <string name="receipts_section_description_1">Sie können in den Kontakt- und Gruppeneinstellungen überschrieben werden.</string> <string name="receipts_section_contacts">Kontakte</string> - <string name="receipts_contacts_title_disable">Empfangsbestätigungen deaktivieren\?</string> - <string name="receipts_contacts_title_enable">Empfangsbestätigungen aktivieren\?</string> - <string name="receipts_contacts_override_enabled">Das Senden von Empfangsbestätigungen an %d Kontakte ist aktiviert</string> - <string name="receipts_contacts_enable_for_all">Für Alle aktivieren</string> + <string name="receipts_contacts_title_disable">Bestätigungen deaktivieren\?</string> + <string name="receipts_contacts_title_enable">Bestätigungen aktivieren\?</string> + <string name="receipts_contacts_override_enabled">Das Senden von Bestätigungen an %d Kontakte ist aktiviert</string> + <string name="receipts_contacts_enable_for_all">Für alle aktivieren</string> <string name="settings_section_title_delivery_receipts">EMPFANGSBESTÄTIGUNGEN SENDEN AN</string> <string name="receipts_contacts_disable_keep_overrides">Deaktivieren (vorgenommene Einstellungen bleiben erhalten)</string> - <string name="send_receipts">Empfangsbestätigungen senden</string> + <string name="send_receipts">Bestätigungen senden</string> <string name="v5_2_fix_encryption">Ihre Verbindungen beibehalten</string> <string name="v5_2_message_delivery_receipts">Empfangsbestätigungen für Nachrichten!</string> - <string name="v5_2_message_delivery_receipts_descr">Das zweite Häkchen, welches wir vermisst haben! ✅</string> - <string name="v5_2_fix_encryption_descr">Reparatur der Verschlüsselung nach wiedereinspielen von Backups.</string> + <string name="v5_2_message_delivery_receipts_descr">Wir haben das zweite Häkchen vermisst! ✅</string> + <string name="v5_2_fix_encryption_descr">Reparatur der Verschlüsselung nach Wiedereinspielen von Backups.</string> <string name="v5_2_more_things">Ein paar weitere Dinge</string> <string name="v5_2_disappear_one_message_descr">Auch wenn sie im Chat deaktiviert sind.</string> <string name="v5_2_more_things_descr">- stabilere Zustellung von Nachrichten. @@ -1421,5 +1417,59 @@ \n- und mehr!</string> <string name="dont_enable_receipts">Nicht aktivieren</string> <string name="sending_delivery_receipts_will_be_enabled">Das Senden von Empfangsbestätigungen an alle Kontakte wird aktiviert.</string> - <string name="you_can_enable_delivery_receipts_later_alert">Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren.</string> + <string name="you_can_enable_delivery_receipts_later_alert">Sie können diese später in den Datenschutz- und Sicherheits-Einstellungen der App aktivieren.</string> + <string name="choose_file_title">Datei auswählen</string> + <string name="in_developing_title">Kommt bald!</string> + <string name="no_selected_chat">Kein Chat ausgewählt</string> + <string name="receipts_groups_title_enable">Bestätigungen für Gruppen aktivieren\?</string> + <string name="receipts_section_groups">Kleine Gruppen (max. 20)</string> + <string name="receipts_groups_disable_keep_overrides">Deaktivieren (vorgenommene Gruppeneinstellungen bleiben erhalten)</string> + <string name="receipts_groups_title_disable">Bestätigungen für Gruppen deaktivieren\?</string> + <string name="receipts_groups_enable_for_all">Für alle Gruppen aktivieren</string> + <string name="receipts_groups_enable_keep_overrides">Aktivieren (vorgenommene Gruppeneinstellungen bleiben erhalten)</string> + <string name="receipts_groups_override_disabled">Das Senden von Bestätigungen ist für %d Gruppen deaktiviert</string> + <string name="receipts_groups_override_enabled">Das Senden von Bestätigungen ist für %d Gruppen aktiviert</string> + <string name="receipts_groups_disable_for_all">Für alle Gruppen deaktivieren</string> + <string name="send_receipts_disabled">deaktiviert</string> + <string name="send_receipts_disabled_alert_title">Bestätigungen sind deaktiviert</string> + <string name="no_info_on_delivery">Keine Information über die Zustellung</string> + <string name="delivery">Zustellung</string> + <string name="in_developing_desc">Dieses Feature wird noch nicht unterstützt. Versuchen Sie es mit der nächsten Version.</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled_alert_msg">Es werden keine Empfangsbestätigungen gesendet, da diese Gruppe über %1$d Mitglieder hat.</string> + <string name="connect_via_member_address_alert_desc">An dieses Gruppenmitglied wird eine Verbindungsanfrage gesendet.</string> + <string name="connect_via_member_address_alert_title">Direkt verbinden\?</string> + <string name="connect_via_link_incognito">Inkognito verbinden</string> + <string name="connect_use_current_profile">Nutzen Sie das aktuelle Profil</string> + <string name="connect_use_new_incognito_profile">Nutzen Sie das neue Inkognito-Profil</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Wählen Sie bitte in den App-Einstellungen <b>App-Akkuverbrauch</b> / <b>Unbeschränkt</b> , um Anrufe im Hintergrund zu führen.]]></string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Fügen Sie den erhaltenen Link ein, um sich mit Ihrem Kontakt zu verbinden…</string> + <string name="connect__a_new_random_profile_will_be_shared">Es wird ein neues Zufallsprofil geteilt.</string> + <string name="connect__your_profile_will_be_shared">Ihr Profil %1$s wird geteilt.</string> + <string name="turn_off_battery_optimization_button">Erlauben</string> + <string name="disable_notifications_button">Benachrichtigungen deaktivieren</string> + <string name="system_restricted_background_in_call_title">Keine Anrufe im Hintergrund</string> + <string name="turn_off_system_restriction_button">App-Einstellungen öffnen</string> + <string name="system_restricted_background_desc">SimpleX kann nicht im Hintergrund ablaufen. Sie erhalten Benachrichtigungen nur dann, solange die App läuft.</string> + <string name="system_restricted_background_in_call_desc">Die App kann nach einer Minute im Hintergrund geschlossen werden.</string> + <string name="system_restricted_background_warn"><![CDATA[Wählen Sie bitte in den App-Einstellungen <b>App-Akkuverbrauch</b> / <b>Unbeschränkt</b> , um Benachrichtigungen zu aktivieren.]]></string> + <string name="rcv_group_event_n_members_connected">%s, %s und %d weitere Mitglieder wurden verbunden</string> + <string name="rcv_group_event_2_members_connected">%s und %s wurden verbunden</string> + <string name="rcv_group_event_3_members_connected">%s, %s und %s wurden verbunden</string> + <string name="privacy_message_draft">Nachrichtenentwurf</string> + <string name="privacy_show_last_messages">Letzte Nachrichten anzeigen</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Die Datenbank wird verschlüsselt und das Passwort in den Einstellungen gespeichert.</string> + <string name="you_can_change_it_later">Das zufällige Passwort wird in Klartext in den Einstellungen gespeichert. +\nSie können es später ändern.</string> + <string name="database_encryption_will_be_updated_in_settings">Das Passwort für die Datenbankverschlüsselung wird aktualisiert und in den Einstellungen gespeichert.</string> + <string name="remove_passphrase_from_settings">Passwort aus den Einstellungen entfernen\?</string> + <string name="use_random_passphrase">Zufälliges Passwort verwenden</string> + <string name="save_passphrase_in_settings">Passwort in den Einstellungen sichern</string> + <string name="setup_database_passphrase">Datenbank-Passwort einrichten</string> + <string name="set_database_passphrase">Datenbank-Passwort festlegen</string> + <string name="open_database_folder">Datenbank-Ordner öffnen</string> + <string name="passphrase_will_be_saved_in_settings">Das Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben.</string> + <string name="settings_is_storing_in_clear_text">Das Passwort wurde in Klartext in den Einstellungen gespeichert.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Bitte beachten Sie</b>: Die Nachrichten- und Dateirelais sind per SOCKS Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]></string> + <string name="encrypt_local_files">Lokale Dateien verschlüsseln</string> </resources> \ No newline at end of file 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 6e5b82e74e..7b2aa4e685 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -21,7 +21,6 @@ <string name="chat_preferences_always">siempre</string> <string name="notifications_mode_off_desc">La aplicación sólo puede recibir notificaciones cuando se está ejecutando. No se iniciará ningún servicio en segundo plano.</string> <string name="settings_section_title_icon">ICONO APLICACIÓN</string> - <string name="incognito_random_profile_from_contact_description">Se enviará un perfil aleatorio al contacto del que recibió este enlace</string> <string name="turning_off_service_and_periodic">La optimización de la batería está activa, desactivando el servicio en segundo plano y las solicitudes periódicas de nuevos mensajes. Puedes volver a activarlos en Configuración.</string> <string name="notifications_mode_service_desc">El servicio está siempre en funcionamiento en segundo plano. Las notificaciones se muestran en cuanto haya mensajes nuevos.</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Se puede desactivar en Configuración</b> – las notificaciones se seguirán mostrando mientras la app esté en funcionamiento.]]></string> @@ -57,7 +56,6 @@ <string name="allow_to_send_disappearing">Se permiten mensajes temporales.</string> <string name="keychain_is_storing_securely">Android Keystore se usará para almacenar de forma segura la contraseña - permite que el servicio de notificación funcione.</string> <string name="users_add">Añadir perfil</string> - <string name="incognito_random_profile_description">Se enviará un perfil aleatorio a tu contacto</string> <string name="color_primary">Color</string> <string name="allow_your_contacts_irreversibly_delete">Permites a tus contactos eliminar irreversiblemente los mensajes enviados.</string> <string name="allow_voice_messages_only_if">Se permiten los mensajes de voz pero sólo si tu contacto también los permite.</string> @@ -142,14 +140,14 @@ <string name="group_member_status_announced">conectando (anunciado)</string> <string name="connection_local_display_name">conexión %1$d</string> <string name="connect_via_link_or_qr">Conecta vía enlace / Código QR</string> - <string name="delete_contact_all_messages_deleted_cannot_undo_warning">El contacto y todos los mensajes serán eliminados. ¡No se podrá deshacer!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">El contacto y todos los mensajes serán eliminados. ¡No podrá deshacerse!</string> <string name="icon_descr_contact_checked">Contacto verificado</string> <string name="status_contact_has_e2e_encryption">el contacto dispone de cifrado de extremo a extremo</string> <string name="smp_server_test_disconnect">Desconectar</string> <string name="notification_preview_mode_contact">Contacto</string> <string name="copy_verb">Copiar</string> - <string name="create_your_profile">Crear tu perfil</string> - <string name="always_use_relay">Siempre usar relay</string> + <string name="create_your_profile">Crea tu perfil</string> + <string name="always_use_relay">Usar siempre retransmisor</string> <string name="set_password_to_export_desc">La base de datos está cifrada con una contraseña aleatoria. Cámbiala antes de exportar.</string> <string name="total_files_count_and_size">%d archivo(s) con tamaño total de %s</string> <string name="enable_automatic_deletion_question">¿Activar eliminación automática de mensajes\?</string> @@ -194,7 +192,7 @@ <string name="delete_address__question">¿Eliminar la dirección\?</string> <string name="display_name__field">Nombre mostrado:</string> <string name="callstate_connecting">conectando…</string> - <string name="decentralized">Descentralizado</string> + <string name="decentralized">Descentralizada</string> <string name="database_will_be_encrypted">La base de datos será cifrada.</string> <string name="delete_chat_archive_question">¿Eliminar archivo del chat\?</string> <string name="create_group_link">Crear enlace de grupo</string> @@ -279,7 +277,7 @@ <string name="chat_is_stopped_indication">Chat está detenido</string> <string name="rcv_group_event_changed_member_role">rol de %s cambiado a %s</string> <string name="change_role">Cambiar rol</string> - <string name="v4_5_transport_isolation_descr">Mediante perfil de Chat (por defecto) o por conexión (BETA)</string> + <string name="v4_5_transport_isolation_descr">Mediante perfil (por defecto) o por conexión (BETA)</string> <string name="snd_conn_event_switch_queue_phase_changing">cambiando de servidor…</string> <string name="chat_preferences">Preferencias de Chat</string> <string name="feature_cancelled_item">cancelado %s</string> @@ -438,12 +436,11 @@ <string name="group_full_name_field">Nombre completo del grupo:</string> <string name="group_profile_is_stored_on_members_devices">El perfil de grupo se almacena en los dispositivos, no en los servidores.</string> <string name="icon_descr_help">ayuda</string> - <string name="paste_connection_link_below_to_connect">Pega el enlace que has recibido en el recuadro para conectar con tu contacto.</string> <string name="share_link">Compartir enlace</string> <string name="how_it_works">Cómo funciona</string> <string name="delete_message_cannot_be_undone_warning">El mensaje se eliminará. ¡No podrá deshacerse!</string> - <string name="incognito_info_protects">La función del modo incógnito es proteger la identidad del perfil principal: por cada contacto nuevo se genera un perfil aleatorio.</string> - <string name="turn_off_battery_optimization"><![CDATA[Para poder usarse <b>deshabilita la optimización de batería</b> para SimpleX en el siguiente cuadro de diálogo. De lo contrario las notificaciones estarán desactivadas.]]></string> + <string name="incognito_info_protects">El modo incógnito protege tu privacidad creando un perfil aleatorio por cada contacto.</string> + <string name="turn_off_battery_optimization"><![CDATA[Para usar SimpleX, por favor <b>permite que SimpleX se ejecute en segundo plano</b> en el siguiente cuadro de diálogo. De lo contrario las notificaciones se desactivarán.]]></string> <string name="install_simplex_chat_for_terminal">Instalar terminal para SimpleX Chat</string> <string name="group_invitation_item_description">invitación al grupo %1$s</string> <string name="rcv_group_event_member_added">ha invitado a %1$s</string> @@ -488,7 +485,7 @@ <string name="mark_unread">Marcar como no leído</string> <string name="invalid_QR_code">Código QR inválido</string> <string name="incorrect_code">¡Código de seguridad incorrecto!</string> - <string name="markdown_in_messages">Sintaxis markdown en los mensajes</string> + <string name="markdown_in_messages">Sintaxis Markdown</string> <string name="network_use_onion_hosts_no">No</string> <string name="callstatus_missed">llamada perdida</string> <string name="import_database_confirmation">Importar</string> @@ -529,7 +526,7 @@ <string name="v4_5_italian_interface">Interfaz en italiano</string> <string name="v4_5_message_draft">Borrador de mensaje</string> <string name="v4_5_reduced_battery_usage_descr">¡Pronto habrá más mejoras!</string> - <string name="v4_5_multiple_chat_profiles">Múltiples perfiles de chat</string> + <string name="v4_5_multiple_chat_profiles">Múltiples perfiles</string> <string name="notifications">Notificaciones</string> <string name="no_details">sin detalles</string> <string name="ok">OK</string> @@ -554,7 +551,7 @@ <string name="group_member_role_observer">observador</string> <string name="group_member_role_member">miembro</string> <string name="group_member_status_removed">expulsado</string> - <string name="group_member_status_invited">ha invitado a</string> + <string name="group_member_status_invited">ha sido invitado</string> <string name="no_contacts_to_add">Sin contactos que añadir</string> <string name="new_member_role">Nuevo rol de miembro</string> <string name="initial_member_role">Rol inicial</string> @@ -581,8 +578,7 @@ <string name="leave_group_question">¿Salir del grupo\?</string> <string name="group_member_role_owner">propietario</string> <string name="member_will_be_removed_from_group_cannot_be_undone">El miembro será expulsado del grupo. ¡No podrá deshacerse!</string> - <string name="only_group_owners_can_enable_voice">Sólo los propietarios pueden activar los mensajes de voz.</string> - <string name="group_unsupported_incognito_main_profile_sent">El modo incógnito no se admite aquí, tu perfil principal aparecerá en miembros del grupo</string> + <string name="only_group_owners_can_enable_voice">Sólo los propietarios del grupo pueden activar los mensajes de voz.</string> <string name="icon_descr_more_button">Más</string> <string name="mark_code_verified">Marcar como verificado</string> <string name="smp_servers_invalid_address">¡Dirección de servidor no válida!</string> @@ -618,7 +614,7 @@ <string name="icon_descr_server_status_pending">Pendiente</string> <string name="periodic_notifications">Notificaciones periódicas</string> <string name="store_passphrase_securely">Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes.</string> - <string name="restart_the_app_to_create_a_new_chat_profile">Reinicia la aplicación para crear un nuevo perfil de chat.</string> + <string name="restart_the_app_to_create_a_new_chat_profile">Reinicia la aplicación para crear un perfil nuevo.</string> <string name="restore_database_alert_title">¿Restaurar copia de seguridad de la base de datos\?</string> <string name="onboarding_notifications_mode_title">Notificaciones privadas</string> <string name="image_descr_profile_image">imagen del perfil</string> @@ -721,7 +717,7 @@ <string name="la_notice_title_simplex_lock">Bloqueo SimpleX</string> <string name="auth_unlock">Desbloquear</string> <string name="this_text_is_available_in_settings">Este texto está disponible en Configuración</string> - <string name="switch_receiving_address_desc">La dirección de recepción se cambiará. El cambio se completará cuando el remitente esté en línea.</string> + <string name="switch_receiving_address_desc">La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea.</string> <string name="chat_lock">Bloqueo SimpleX</string> <string name="using_simplex_chat_servers">Usando servidores SimpleX Chat.</string> <string name="network_session_mode_transport_isolation">Aislamiento de transporte</string> @@ -753,20 +749,20 @@ <string name="share_invitation_link">Compartir enlace de un uso</string> <string name="update_network_session_mode_question">¿Actualizar el modo de aislamiento de transporte\?</string> <string name="icon_descr_speaker_on">Altavoz activado</string> - <string name="stop_chat_to_enable_database_actions">Para habilitar las acciones sobre la base de datos, previamente debes detener Chat</string> + <string name="stop_chat_to_enable_database_actions">Detén SimpleX para habilitar las acciones sobre la base de datos.</string> <string name="connection_you_accepted_will_be_cancelled">¡La conexión que has aceptado se cancelará!</string> <string name="database_initialization_error_desc">La base de datos no funciona correctamente. Pulsa para saber más</string> <string name="moderate_message_will_be_marked_warning">El mensaje será marcado como moderado para todos los miembros.</string> - <string name="next_generation_of_private_messaging">La próxima generación de mensajería privada</string> + <string name="next_generation_of_private_messaging">La nueva generación de mensajería privada</string> <string name="delete_files_and_media_desc">Esta acción no se puede deshacer. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán.</string> <string name="enable_automatic_deletion_message">Esta acción no se puede deshacer. Se eliminarán los mensajes enviados y recibidos anteriores a la selección. Puede tardar varios minutos.</string> <string name="messages_section_description">Esta configuración se aplica a los mensajes del perfil actual</string> <string name="this_string_is_not_a_connection_link">¡Esta cadena no es un enlace de conexión!</string> - <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery">Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo planoSimpleX</b>, usa un pequeño porcentaje de la batería al día.</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Para preservar tu privacidad, en lugar de notificaciones automáticas la aplicación cuenta con un <b>servicio en segundo planoSimpleX</b>, usa un pequeño porcentaje de la batería al día.]]></string> <string name="icon_descr_settings">Configuración</string> <string name="icon_descr_speaker_off">Altavoz desactivado</string> <string name="add_contact_or_create_group">Inciar chat nuevo</string> - <string name="stop_chat_to_export_import_or_delete_chat_database">Para poder exportar, importar o eliminar la base de datos primero debes detener Chat. Durante el tiempo que esté detenido no podrás recibir ni enviar mensajes.</string> + <string name="stop_chat_to_export_import_or_delete_chat_database">Para exportar, importar o eliminar la base de datos debes detener Chat. Durante la parada no podrás recibir o enviar mensajes.</string> <string name="thank_you_for_installing_simplex">Gracias por instalar SimpleX Chat!</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">Para proteger la privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos.</string> <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Para proteger tu información, activa Bloqueo SimpleX. @@ -794,7 +790,6 @@ <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad.</string> <string name="first_platform_without_user_ids">La primera plataforma sin identificadores de usuario: diseñada para la privacidad.</string> <string name="alert_message_no_group">Este grupo ya no existe.</string> - <string name="incognito_info_find">Para conocer el perfil usado en una conexión en modo incógnito, pulsa el nombre del contacto o del grupo en la parte superior del chat.</string> <string name="accept_feature_set_1_day">Establecer 1 día</string> <string name="v4_4_french_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> <string name="v4_5_italian_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> @@ -868,18 +863,18 @@ <string name="v4_4_verify_connection_security">Comprobar la seguridad de la conexión</string> <string name="you_are_already_connected_to_vName_via_this_link">¡Ya estás conectado a %1$s.</string> <string name="welcome">¡Bienvenido!</string> - <string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil Chat será enviado + <string name="your_chat_profile_will_be_sent_to_your_contact">Tu perfil será enviado \na tu contacto</string> <string name="your_ICE_servers">Servidores ICE</string> <string name="you_rejected_group_invitation">Has rechazado la invitación del grupo.</string> <string name="snd_group_event_changed_member_role">has cambiado el rol de %s a %s</string> - <string name="call_connection_via_relay">mediante relay</string> + <string name="call_connection_via_relay">mediante retransmisor</string> <string name="contact_wants_to_connect_with_you">¡quiere contactar contigo!</string> <string name="voice_message">Mensaje de voz</string> <string name="waiting_for_image">Esperando imagen</string> <string name="webrtc_ice_servers">Servidores WebRTC ICE</string> <string name="contact_wants_to_connect_via_call">%1$s quiere conectarse contigo mediante</string> - <string name="failed_to_create_user_duplicate_desc">Tienes un perfil de chat con el mismo nombre mostrado. Debes elegir otro nombre.</string> + <string name="failed_to_create_user_duplicate_desc">Ya tienes un perfil con este nombre mostrado. Por favor, elige otro nombre.</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[También puedes conectarte pulsando el enlace. Si se abre en el navegador, pulsa en <b>Abrir en aplicación móvil</b>.]]></string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Puedes <font color="#0088ff">ponerte en contacto con los desarrolladores de SimpleX Chat para consultas y para recibir actualizaciones</font>.]]></string> <string name="observer_cant_send_message_title">¡No puedes enviar mensajes!</string> @@ -890,7 +885,7 @@ <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">La base de datos actual será ELIMINADA y SUSTITUIDA por la importada. \nEsta acción no se puede deshacer. Tu perfil, contactos, mensajes y archivos se perderán irreversiblemente.</string> <string name="incognito_random_profile">Tu perfil aleatorio</string> - <string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando se acepte tu solicitud de conexión, por favor espere o compruébalo más tarde.</string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde.</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde.</string> <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano.</string> <string name="invite_prohibited_description">Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal</string> @@ -914,12 +909,12 @@ <string name="you_are_observer">Tu rol es observador</string> <string name="verify_security_code">Comprobar código de seguridad</string> <string name="you_accepted_connection">Has aceptado la conexión</string> - <string name="you_invited_your_contact">Has invitado a tu contacto</string> + <string name="you_invited_a_contact">Has invitado a tu contacto</string> <string name="you_will_be_connected_when_group_host_device_is_online">Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o compruébalo más tarde.</string> - <string name="your_settings">Tu configuración</string> + <string name="your_settings">Configuración</string> <string name="your_SMP_servers">Servidores SMP</string> <string name="you_control_your_chat">¡Tú controlas tu chat!</string> - <string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes entregados se almacenan en tu dispositivo.</string> + <string name="your_profile_is_stored_on_your_device">Tu perfil, contactos y mensajes se almacenan en tu dispositivo.</string> <string name="callstate_waiting_for_answer">esperando respuesta…</string> <string name="callstate_waiting_for_confirmation">esperando confirmación…</string> <string name="onboarding_notifications_mode_off">Cuando la aplicación se está ejecutando</string> @@ -928,7 +923,7 @@ <string name="your_ice_servers">Servidores ICE</string> <string name="your_privacy">Privacidad</string> <string name="settings_section_title_you">MIS DATOS</string> - <string name="your_chat_database">Base de datos Chat</string> + <string name="your_chat_database">Base de datos</string> <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puedes iniciar el chat en Configuración / Base de datos o reiniciando la aplicación.</string> <string name="you_sent_group_invitation">Has enviado una invitación de grupo</string> <string name="num_contacts_selected">%d contacto(s) seleccionado(s)</string> @@ -954,12 +949,11 @@ <string name="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á.</string> <string name="view_security_code">Mostrar código de seguridad</string> <string name="you_need_to_allow_to_send_voice">Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos.</string> - <string name="voice_messages_prohibited">¡Mensajes de voz prohibidos!</string> - <string name="group_main_profile_sent">Tu perfil Chat será enviado a los miembros del grupo</string> + <string name="voice_messages_prohibited">¡Mensajes de voz no permitidos!</string> + <string name="group_main_profile_sent">Tu perfil será enviado a los miembros del grupo</string> <string name="icon_descr_address">Dirección SimpleX</string> <string name="image_descr_simplex_logo">Logo SimpleX</string> <string name="icon_descr_simplex_team">Equipo SimpleX</string> - <string name="your_profile_will_be_sent">Tu perfil Chat se enviará a tu contacto</string> <string name="your_chat_profiles">Mis perfiles</string> <string name="your_simplex_contact_address">Mi dirección SimpleX</string> <string name="smp_servers_your_server">Tu servidor</string> @@ -974,8 +968,8 @@ <string name="save_profile_password">Guardar contraseña de perfil</string> <string name="password_to_show">Contraseña para hacerlo visible</string> <string name="error_saving_user_password">Error al guardar contraseña de usuario</string> - <string name="relay_server_if_necessary">El relay sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</string> - <string name="relay_server_protects_ip">El servidor relay protege tu IP pero puede ver la duración de la llamada.</string> + <string name="relay_server_if_necessary">El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP.</string> + <string name="relay_server_protects_ip">El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada.</string> <string name="cant_delete_user_profile">¡No se puede eliminar el perfil!</string> <string name="enter_password_to_show">Introduce la contraseña</string> <string name="user_hide">Ocultar</string> @@ -987,11 +981,11 @@ <string name="button_welcome_message">Mensaje de bienvenida</string> <string name="group_welcome_title">Mensaje de bienvenida</string> <string name="should_be_at_least_one_profile">Debe haber al menos un perfil de usuario.</string> - <string name="make_profile_private">¡Hacer un perfil privado!</string> + <string name="make_profile_private">¡Hacer perfil privado!</string> <string name="dont_show_again">No mostrar de nuevo</string> <string name="muted_when_inactive">¡Silenciado cuando está inactivo!</string> <string name="v4_6_group_moderation">Moderación de grupos</string> - <string name="v4_6_hidden_chat_profiles">Perfiles Chat ocultos</string> + <string name="v4_6_hidden_chat_profiles">Perfiles ocultos</string> <string name="v4_6_hidden_chat_profiles_descr">¡Protege tus perfiles con contraseña!</string> <string name="v4_6_audio_video_calls_descr">Ahora con soporte bluetooth y otras mejoras.</string> <string name="v4_6_group_welcome_message_descr">¡Guarda un mensaje para ser mostrado a los miembros nuevos!</string> @@ -1010,7 +1004,7 @@ <string name="v4_6_group_moderation_descr">Ahora los administradores pueden: \n- borrar mensajes de los miembros. \n- desactivar el rol miembro (a rol \"observador\")</string> - <string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda de la página Mis perfiles.</string> + <string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda del menú Mis perfiles.</string> <string name="database_upgrade">Actualización de la base de datos</string> <string name="database_downgrade">Volviendo a versión anterior de la base de datos</string> <string name="invalid_migration_confirmation">Confirmación de migración no válida</string> @@ -1031,7 +1025,7 @@ <string name="show_dev_options">Mostrar:</string> <string name="delete_chat_profile">Eliminar perfil de chat</string> <string name="profile_password">Contraseña del perfil</string> - <string name="unhide_chat_profile">Mostrar perfil de chat</string> + <string name="unhide_chat_profile">Mostrar perfil oculto</string> <string name="unhide_profile">Mostrar perfil</string> <string name="delete_profile">Eliminar perfil</string> <string name="video_descr">Vídeo</string> @@ -1128,11 +1122,11 @@ <string name="v5_0_polish_interface_descr">¡Gracias a los colaboradores! Contribuye a través de Weblate.</string> <string name="v5_0_large_files_support">Vídeos y archivos de hasta 1Gb</string> <string name="v5_0_large_files_support_descr">¡Rápido y sin necesidad de esperar a que el remitente esté en línea!</string> - <string name="auth_open_chat_profiles">Abrir perfiles de chat</string> + <string name="auth_open_chat_profiles">Abrir perfiles</string> <string name="learn_more">Más información</string> <string name="if_you_cant_meet_in_person">Si no puedes reunirte en persona, **muestra el código QR por videollamada**, o comparte el enlace.</string> <string name="scan_qr_to_connect_to_contact">Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación.</string> - <string name="create_simplex_address">Crear dirección SimpleX</string> + <string name="create_simplex_address">Crear tu dirección SimpleX</string> <string name="auto_accept_contact">Auto aceptar</string> <string name="group_welcome_preview">Vista previa</string> <string name="opening_database">Abriendo base de datos…</string> @@ -1150,15 +1144,15 @@ <string name="export_theme">Exportar tema</string> <string name="color_surface">Menús y alertas</string> <string name="add_address_to_your_profile">Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos.</string> - <string name="learn_more_about_address">Acerca de dirección SimpleX</string> + <string name="learn_more_about_address">Acerca de la dirección SimpleX</string> <string name="address_section_title">Dirección</string> <string name="all_your_contacts_will_remain_connected_update_sent">Todos tus contactos permanecerán conectados. La actualización del perfil se enviará a tus contactos.</string> <string name="continue_to_next_step">Continuar</string> <string name="dark_theme">Tema oscuro</string> <string name="customize_theme_title">Personalizar tema</string> <string name="enter_welcome_message_optional">Introduce mensaje de bienvenida… (opcional)</string> - <string name="create_address_and_let_people_connect">Crear una dirección para que otras personas se puedan conectar contigo.</string> - <string name="dont_create_address">No crear dirección</string> + <string name="create_address_and_let_people_connect">Crea una dirección para que otras personas puedan conectar contigo.</string> + <string name="dont_create_address">No crear dirección SimpleX</string> <string name="email_invite_body">¡Hola! \nConecta conmigo a través de SimpleX Chat: %s</string> <string name="import_theme">Importar tema</string> @@ -1176,7 +1170,7 @@ <string name="stop_sharing">Dejar de compartir</string> <string name="stop_sharing_address">¿Dejar de compartir la dirección\?</string> <string name="theme_colors_section_title">COLORES DEL TEMA</string> - <string name="you_can_create_it_later">Puedes crearlo más tarde</string> + <string name="you_can_create_it_later">Puedes crearla más tarde</string> <string name="share_address_with_contacts_question">¿Compartir la dirección con los contactos\?</string> <string name="share_with_contacts">Compartir con contactos</string> <string name="color_title">Título</string> @@ -1278,19 +1272,19 @@ <string name="unfavorite_chat">No favorito</string> <string name="abort_switch_receiving_address">Cancelar cambio de dirección</string> <string name="files_and_media">Archivos y multimedia</string> - <string name="prohibit_sending_files">No permitir el envío de archivos y multimedia.</string> + <string name="prohibit_sending_files">No se permite el envío de archivos y multimedia.</string> <string name="files_are_prohibited_in_group">No se permiten archivos y multimedia en este grupo.</string> <string name="group_members_can_send_files">Los miembros del grupo pueden enviar archivos y multimedia.</string> <string name="allow_to_send_files">Se permite enviar archivos y multimedia</string> <string name="favorite_chat">Favorito</string> - <string name="only_owners_can_enable_files_and_media">Sólo los propietarios pueden activar archivos y multimedia.</string> + <string name="only_owners_can_enable_files_and_media">Sólo los propietarios del grupo pueden activar los archivos y multimedia.</string> <string name="network_option_protocol_timeout_per_kb">Timeout de protocolo por KB</string> <string name="snd_conn_event_ratchet_sync_allowed">renegociación de cifrado permitida para %s</string> <string name="conn_event_ratchet_sync_agreed">cifrado acordado</string> <string name="conn_event_ratchet_sync_ok">cifrado ok</string> <string name="conn_event_ratchet_sync_required">se requiere renegociar el cifrado</string> <string name="error_synchronizing_connection">Error al sincronizar conexión</string> - <string name="fix_connection_not_supported_by_contact">Reparación no compatible con el contacto</string> + <string name="fix_connection_not_supported_by_contact">Corrección no compatible con el contacto</string> <string name="sync_connection_force_confirm">Renegociar</string> <string name="fix_connection_not_supported_by_group_member">Reparación no compatible con miembro del grupo</string> <string name="sync_connection_force_desc">El cifrado funciona y un cifrado nuevo no es necesario. ¡Podría dar lugar a errores de conexión!</string> @@ -1312,7 +1306,7 @@ <string name="sending_delivery_receipts_will_be_enabled_all_profiles">El envío de confirmaciones de entrega se activará para todos los contactos en todos los perfiles visibles.</string> <string name="receipts_section_contacts">Contactos</string> <string name="delivery_receipts_title">¡Confirmación de entrega!</string> - <string name="receipts_contacts_disable_keep_overrides">Desactivar (mantener anulaciones)</string> + <string name="receipts_contacts_disable_keep_overrides">Desactivar (conservando anulaciones)</string> <string name="v5_2_favourites_filter">Encontrar chats mas rápido</string> <string name="v5_2_disappear_one_message">Escribir un mensaje temporal</string> <string name="receipts_contacts_override_disabled">El envío de confirmaciones está desactivado para %d contactos</string> @@ -1322,7 +1316,7 @@ <string name="enable_receipts_all">Activar</string> <string name="receipts_contacts_title_disable">¿Desactivar confirmaciones\?</string> <string name="receipts_contacts_title_enable">¿Activar confirmaciones\?</string> - <string name="receipts_section_description_1">Se pueden anular en la configuración de los contactos</string> + <string name="receipts_section_description_1">Se pueden anular en la configuración de grupos y contactos.</string> <string name="receipts_contacts_enable_for_all">Activar para todos</string> <string name="receipts_contacts_enable_keep_overrides">Activar (conservar anulaciones)</string> <string name="receipts_contacts_disable_for_all">Desactivar para todos</string> @@ -1333,7 +1327,7 @@ <string name="v5_2_disappear_one_message_descr">Incluso si está desactivado para la conversación.</string> <string name="v5_2_favourites_filter_descr">Filtrar chats no leídos y favoritos.</string> <string name="v5_2_fix_encryption_descr">Reparar el cifrado tras restaurar copias de seguridad.</string> - <string name="v5_2_fix_encryption">Mantén tus conexiones</string> + <string name="v5_2_fix_encryption">Conserva tus conexiones</string> <string name="v5_2_message_delivery_receipts">¡Confirmación de entrega de mensajes!</string> <string name="v5_2_more_things_descr">- entrega de mensajes más estable. \n- grupos un poco mejores. @@ -1343,4 +1337,56 @@ <string name="you_can_enable_delivery_receipts_later">Puedes activar más tarde en Configuración</string> <string name="you_can_enable_delivery_receipts_later_alert">Puedes activarlos más tarde en la configuración de Privacidad y Seguridad.</string> <string name="v5_2_more_things">Algunas cosas más</string> + <string name="choose_file_title">Selecciona un archivo</string> + <string name="no_selected_chat">Ningún chat seleccionado</string> + <string name="in_developing_title">¡Muy pronto!</string> + <string name="in_developing_desc">Esta función no está disponible aún. Prueba en la próxima versión.</string> + <string name="delivery">Entrega</string> + <string name="no_info_on_delivery">Sin información de entrega</string> + <string name="receipts_groups_title_enable">¿Permitir confirmaciones para grupos\?</string> + <string name="receipts_groups_override_disabled">Las confirmaciones están deshabilitadas para los grupos %d</string> + <string name="receipts_groups_override_enabled">Las confirmaciones están activadas para %d grupos</string> + <string name="receipts_section_groups">Grupos pequeños (máx. 20)</string> + <string name="receipts_groups_enable_for_all">Activar para todos los grupos</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled">desactivado</string> + <string name="receipts_groups_disable_for_all">Desactivado para todos los grupos</string> + <string name="receipts_groups_disable_keep_overrides">Desactivar (conservando anulaciones de grupo)</string> + <string name="receipts_groups_title_disable">¿Desactivar confirmaciones para grupos\?</string> + <string name="receipts_groups_enable_keep_overrides">Activar (conservar anulaciones de grupo)</string> + <string name="send_receipts_disabled_alert_title">Confirmaciones desactivadas</string> + <string name="send_receipts_disabled_alert_msg">Este grupo tiene más de %1$d miembros, no se enviarán confirmaciones de entrega.</string> + <string name="connect_via_member_address_alert_title">¿Conectar directamente\?</string> + <string name="connect_via_member_address_alert_desc">La solicitud se enviará a este miembro del grupo.</string> + <string name="connect_via_link_incognito">Conectar en incógnito</string> + <string name="turn_off_battery_optimization_button">Permitir</string> + <string name="turn_off_system_restriction_button">Abrir configuración</string> + <string name="connect__a_new_random_profile_will_be_shared">Se compartirá un perfil nuevo aleatorio.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Pega el enlace recibido para conectar con tu contacto…</string> + <string name="connect__your_profile_will_be_shared">Tu perfil %1$s será compartido.</string> + <string name="disable_notifications_button">Desactivar notificaciones</string> + <string name="system_restricted_background_in_call_title">Sin llamadas en segundo plano.</string> + <string name="system_restricted_background_desc">SimpleX no puede funcionar en segundo plano. Sólo recibirás notificaciones con la aplicación abierta.</string> + <string name="system_restricted_background_in_call_desc">La aplicación podría cerrarse después de un minuto en segundo plano.</string> + <string name="system_restricted_background_warn"><![CDATA[Para activar las notificaciones, por favor selecciona <b>Uso de batería de la aplicación</b> / <b>No restringido</b> en las configuraciónes de la aplicación.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Para realizar llamadas en segundo plano, por favor selecciona <b>Uso de batería de la aplicación</b> / <b>No restringido</b> en las configuraciónes de la aplicación.]]></string> + <string name="connect_use_current_profile">Usar perfil actual</string> + <string name="connect_use_new_incognito_profile">Usar nuevo perfil incógnito</string> + <string name="privacy_show_last_messages">Mostrar último mensaje</string> + <string name="rcv_group_event_n_members_connected">%s, %s y %d miembros más conectados</string> + <string name="rcv_group_event_3_members_connected">%s, %s y %s conectados</string> + <string name="rcv_group_event_2_members_connected">%s y %s conectados</string> + <string name="privacy_message_draft">Borrador de mensaje</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">La base de datos será cifrada y la contraseña se guardará en Configuración</string> + <string name="you_can_change_it_later">La contraseña aleatoria se almacenará en Configuración como texto plano. +\nPuedes cambiarlo más tarde.</string> + <string name="database_encryption_will_be_updated_in_settings">La contraseña para el cifrado de la base de datos se actualizará y almacenará en Configuración</string> + <string name="remove_passphrase_from_settings">Eliminar contraseña de configuración\?</string> + <string name="use_random_passphrase">Usar contraseña aleatoria</string> + <string name="save_passphrase_in_settings">Guardar contraseña en configuración</string> + <string name="setup_database_passphrase">Configuración contraseña base de datos</string> + <string name="set_database_passphrase">Escribe una contraseña para la base de datos</string> + <string name="open_database_folder">Abrir carpeta base de datos</string> + <string name="passphrase_will_be_saved_in_settings">La contraseña se almacenará en configuración como texto plano después de cambiarla o reiniciar la aplicación.</string> + <string name="settings_is_storing_in_clear_text">La contraseña está almacenada en configuración como texto plano.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index d4e5df7055..b0280d84ae 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -82,7 +82,7 @@ <string name="info_row_connection">Yhteys</string> <string name="ttl_d">%dd</string> <string name="ttl_days">%d päivää</string> - <string name="disappearing_prohibited_in_this_chat">Tuhoutuvat viestit ovat kiellettyjä tässä keskustelussa.</string> + <string name="disappearing_prohibited_in_this_chat">Katoavat viestit ovat kiellettyjä tässä keskustelussa.</string> <string name="network_session_mode_user_description"><![CDATA[Erillistä TCP-yhteyttä (ja SOCKS-tunnistetietoja) käytetään <b>jokaisessa käyttämässäsi sovelluksen chat-profiilissa</b>.]]></string> <string name="app_version_code">Sovellusversio: %s</string> <string name="delete_address">Poista osoite</string> @@ -140,7 +140,7 @@ <string name="delete_verb">Poista</string> <string name="delete_message__question">Poista viesti\?</string> <string name="group_connection_pending">yhdistää…</string> - <string name="disappearing_message">Tuhoutuva viesti</string> + <string name="disappearing_message">Katoava viesti</string> <string name="send_disappearing_message_custom_time">Mukautettu aika</string> <string name="delete_contact_menu_action">Poista</string> <string name="delete_group_menu_action">Poista</string> @@ -158,7 +158,7 @@ <string name="settings_developer_tools">Kehittäjän työkalut</string> <string name="cannot_access_keychain">Ei pääsyä Keystoreen tietokannan salasanan tallentamiseksi</string> <string name="share_text_database_id">Tietokannan tunnus: %d</string> - <string name="info_row_disappears_at">Tuhoutuu klo</string> + <string name="info_row_disappears_at">Katoaa klo</string> <string name="chat_database_deleted">Keskustelujen tietokanta poistettu</string> <string name="delete_chat_profile_question">Poista keskusteluprofiili\?</string> <string name="delete_messages_after">Poista viestit tämän jälkeen</string> @@ -187,19 +187,19 @@ <string name="audio_call_no_encryption">äänipuhelu (ei e2e-salattu)</string> <string name="always_use_relay">Käytä aina relettä</string> <string name="allow_your_contacts_to_send_disappearing_messages">Salli kontaktiesi lähettää katoavia viestejä.</string> - <string name="timed_messages">Tuhoutuvat viestit</string> + <string name="timed_messages">Katoavat viestit</string> <string name="icon_descr_context">Kontekstikuvake</string> <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Skannaa QR-koodi</b>: muodostaaksesi yhteyden kontaktiisi, joka näyttää QR-koodin sinulle.]]></string> <string name="icon_descr_cancel_live_message">Peruuta live-viesti</string> <string name="configure_ICE_servers">Määritä ICE-palvelimet</string> <string name="add_address_to_your_profile">Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi.</string> - <string name="all_your_contacts_will_remain_connected_update_sent">Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään yhteystietoihisi.</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Kaikki kontaktisi pysyvät yhteydessä. Profiilipäivitys lähetetään kontakteillesi.</string> <string name="create_simplex_address">Luo SimpleX-osoite</string> <string name="accept_call_on_lock_screen">Hyväksy</string> <string name="settings_audio_video_calls">Ääni- ja videopuhelut</string> <string name="call_on_lock_screen">Puhelut lukitusnäytöllä:</string> <string name="conn_level_desc_direct">suora</string> - <string name="disappearing_messages_are_prohibited">Tuhoutuvat viestit ovat kiellettyjä tässä ryhmässä.</string> + <string name="disappearing_messages_are_prohibited">Katoavat viestit ovat kiellettyjä tässä ryhmässä.</string> <string name="server_connected">yhdistetty</string> <string name="display_name_connecting">yhdistää…</string> <string name="display_name_connection_established">yhteys luotu</string> @@ -237,12 +237,10 @@ <string name="callstatus_calling">soittaa…</string> <string name="delete_chat_profile">Poista keskusteluprofiili</string> <string name="delete_profile">Poista profiili</string> - <string name="incognito_random_profile_from_contact_description">Satunnainen profiili lähetetään kontaktille, jolta sait tämän linkin</string> - <string name="incognito_random_profile_description">Satunnainen profiili lähetetään kontaktillesi</string> <string name="both_you_and_your_contact_can_send_disappearing">Sekä sinä että kontaktisi voitte lähettää katoavia viestejä.</string> <string name="chat_preferences_contact_allows">Kontakti sallii</string> <string name="chat_preferences_default">oletus (%s)</string> - <string name="v4_4_disappearing_messages">Tuhoutuvat viestit</string> + <string name="v4_4_disappearing_messages">Katoavat viestit</string> <string name="copied">Kopioitu leikepöydälle</string> <string name="share_one_time_link">Luo kertaluonteinen kutsulinkki</string> <string name="mtr_error_no_down_migration">tietokantaversio on uudempi kuin sovellus, mutta ei alaspäin siirtymistä: %s</string> @@ -338,7 +336,7 @@ <string name="change_verb">Muuta</string> <string name="item_info_current">(nykyinen)</string> <string name="share_text_deleted_at">Poistettu: %s</string> - <string name="share_text_disappears_at">Tuhoutuu klo: %s</string> + <string name="share_text_disappears_at">Katoaa klo: %s</string> <string name="create_secret_group_title">Luo salainen ryhmä</string> <string name="chat_preferences_always">aina</string> <string name="cant_delete_user_profile">Käyttäjäprofiilia ei voi poistaa!</string> @@ -408,7 +406,7 @@ <string name="join_group_question">Liity ryhmään\?</string> <string name="network_option_enable_tcp_keep_alive">Ota TCP-säilytys käyttöön</string> <string name="incognito">Incognito</string> - <string name="incognito_info_protects">Incognito-tila suojaa pääprofiilisi nimen ja kuvan yksityisyyttä – jokaiselle uudelle yhteyshenkilölle luodaan uusi satunnainen profiili.</string> + <string name="incognito_info_protects">Incognito-tila suojaa yksityisyyttäsi käyttämällä uutta satunnaista profiilia jokaiselle kontaktille.</string> <string name="ttl_mth">%dmth</string> <string name="ttl_m">%dm</string> <string name="v4_6_group_welcome_message">Ryhmän tervetuloviesti</string> @@ -424,7 +422,6 @@ <string name="initial_member_role">Alkuperäinen rooli</string> <string name="enter_welcome_message">Kirjoita tervetuloviesti…</string> <string name="error_saving_group_profile">Virhe ryhmäprofiilin tallentamisessa</string> - <string name="group_unsupported_incognito_main_profile_sent">Incognito-tilaa ei tueta tässä - pääprofiilisi lähetetään ryhmän jäsenille</string> <string name="import_theme_error">Virhe teeman tuonnissa</string> <string name="feature_enabled">käytössä</string> <string name="feature_enabled_for_contact">käytössä kontaktille</string> @@ -454,7 +451,7 @@ <string name="import_database_question">Tuo keskustelujen-tietokanta\?</string> <string name="file_with_path">Tiedosto: %s</string> <string name="error_removing_member">Virhe poistettaessa jäsentä</string> - <string name="message_deletion_prohibited">Viestien peruuttamaton poistaminen on kielletty tässä keskustelussa.</string> + <string name="message_deletion_prohibited">Viestien peruuttamaton poisto on kielletty tässä keskustelussa.</string> <string name="join_group_button">Liity</string> <string name="join_group_incognito_button">Liity incognito-tilassa</string> <string name="joining_group">Liittyy ryhmään</string> @@ -477,10 +474,10 @@ <string name="image_will_be_received_when_contact_is_online">Kuva vastaanotetaan, kun kontaktisi on verkossa, odota tai tarkista myöhemmin!</string> <string name="if_you_cant_meet_in_person">Jos et voi tavata henkilökohtaisesti, näytä QR-koodi videopuhelussa tai jaa linkki.</string> <string name="onboarding_notifications_mode_subtitle">Voit muuttaa sitä myöhemmin asetuksista.</string> - <string name="encrypt_database_question">Salataanko tietokanta\?</string> + <string name="encrypt_database_question">Salaa tietokanta\?</string> <string name="button_edit_group_profile">Muokkaa ryhmäprofiilia</string> <string name="delete_group_for_all_members_cannot_undo_warning">Ryhmä poistetaan kaikilta jäseniltä - tätä ei voi kumota!</string> - <string name="delete_group_for_self_cannot_undo_warning">Ryhmä poistetaan sinulta - tätä ei voi peruuttaa!</string> + <string name="delete_group_for_self_cannot_undo_warning">Ryhmä poistetaan sinulta - tätä ei voi perua!</string> <string name="error_creating_link_for_group">Virhe ryhmälinkin luomisessa</string> <string name="info_row_group">Ryhmä</string> <string name="incognito_info_allows">Se mahdollistaa useiden nimettömien yhteyksien muodostamisen yhdessä keskusteluprofiilissa ilman, että niiden välillä on jaettuja tietoja.</string> @@ -517,7 +514,7 @@ <string name="onboarding_notifications_mode_service">Välitön</string> <string name="encrypted_audio_call">e2e-salattu äänipuhelu</string> <string name="allow_accepting_calls_from_lock_screen">Ota puhelut käyttöön lukitusnäytöltä asetuksista.</string> - <string name="status_e2e_encrypted">e2e salattu</string> + <string name="status_e2e_encrypted">e2e-salattu</string> <string name="settings_section_title_incognito">Incognito-tila</string> <string name="error_with_info">Virhe: %s</string> <string name="alert_message_group_invitation_expired">Ryhmäkutsu ei ole enää voimassa, lähettäjä poisti sen.</string> @@ -546,15 +543,15 @@ <string name="downgrade_and_open_chat">Alenna ja avaa chat</string> <string name="icon_descr_group_inactive">Ei-aktiivinen ryhmä</string> <string name="v4_3_improved_privacy_and_security_desc">Piilota sovellusnäyttö viimeisimmissä sovelluksissa.</string> - <string name="v4_5_italian_interface">Italian käyttöliittymä</string> - <string name="v5_0_large_files_support_descr">Nopea ja ei odota, kunnes lähettäjä on online-tilassa!</string> + <string name="v4_5_italian_interface">Italialainen käyttöliittymä</string> + <string name="v5_0_large_files_support_descr">Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!</string> <string name="v4_6_reduced_battery_usage">Entisestä vähentynyt akun käyttö</string> <string name="v5_1_japanese_portuguese_interface">Japanin ja portugalin käyttöliittymä</string> <string name="error_saving_file">Virhe tiedoston tallentamisessa</string> <string name="icon_descr_help">apua</string> <string name="incorrect_code">Väärä turvakoodi!</string> <string name="error_sending_message">Virhe viestin lähettämisessä</string> - <string name="turn_off_battery_optimization"><![CDATA[Jotta voit käyttää sitä, <b>poista akun optimointi käytöstä</b> kohteelle SimpleX seuraavassa valintaikkunassa. Muussa tapauksessa ilmoitukset poistetaan käytöstä.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Käyttääksesi sitä, <b> salli SimpleX:n toimia taustalla </b> seuraavassa ikkunassa. Muutoin ilmoitukset poistetaan käytöstä.]]></string> <string name="notification_preview_mode_hidden">Piilotettu</string> <string name="la_immediately">Heti</string> <string name="la_enter_app_passcode">Syötä pääsykoodi</string> @@ -573,7 +570,7 @@ <string name="enable_lock">Ota lukitus käyttöön</string> <string name="group_invitation_item_description">kutsu ryhmään %1$s</string> <string name="icon_descr_add_members">Kutsu jäseniä</string> - <string name="group_invitation_expired">Ryhmäkutsu on vanhentunut</string> + <string name="group_invitation_expired">Vanhentunut ryhmäkutsu</string> <string name="rcv_group_event_member_added">kutsuttu %1$s</string> <string name="group_full_name_field">Ryhmän koko nimi:</string> <string name="full_name_optional__prompt">Koko nimi (valinnainen)</string> @@ -595,7 +592,7 @@ <string name="section_title_for_console">KONSOLIIN</string> <string name="group_member_status_group_deleted">poistettu ryhmä</string> <string name="snd_group_event_group_profile_updated">ryhmäprofiili päivitetty</string> - <string name="alert_title_group_invitation_expired">Kutsu on vanhentunut!</string> + <string name="alert_title_group_invitation_expired">Vanhentunut kutsu!</string> <string name="group_member_status_invited">kutsuttu</string> <string name="conn_level_desc_indirect">epäsuora (%1$s)</string> <string name="group_display_name_field">Ryhmän näyttönimi:</string> @@ -605,10 +602,10 @@ <string name="group_members_can_delete">Ryhmän jäsenet voivat poistaa lähetetyt viestit peruuttamattomasti.</string> <string name="group_members_can_send_dms">Ryhmän jäsenet voivat lähettää suoraviestejä.</string> <string name="group_members_can_send_voice">Ryhmän jäsenet voivat lähettää ääniviestejä.</string> - <string name="message_deletion_prohibited_in_chat">Viestien peruuttamaton poistaminen on kielletty tässä ryhmässä.</string> + <string name="message_deletion_prohibited_in_chat">Viestien peruuttamaton poisto on kielletty tässä ryhmässä.</string> <string name="ttl_months">%d kuukautta</string> <string name="ttl_sec">%d sek</string> - <string name="v5_1_message_reactions_descr">Vihdoinkin meillä on ne! 🚀</string> + <string name="v5_1_message_reactions_descr">Vihdoinkin meillä! 🚀</string> <string name="custom_time_unit_hours">tuntia</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki.</string> <string name="sender_may_have_deleted_the_connection_request">Lähettäjä on saattanut poistaa yhteyspyynnön.</string> @@ -622,7 +619,7 @@ <string name="notification_preview_mode_message">Viestin teksti</string> <string name="notification_preview_mode_contact_desc">Näytä vain kontakti</string> <string name="notification_preview_new_message">uusi viesti</string> - <string name="la_notice_title_simplex_lock">Simplex Lock</string> + <string name="la_notice_title_simplex_lock">SimpleX Lock</string> <string name="auth_simplex_lock_turned_on">SimpleX Lock päällä</string> <string name="auth_open_chat_console">Avaa keskustelukonsoli</string> <string name="message_delivery_error_title">Viestin toimitusvirhe</string> @@ -851,7 +848,7 @@ <string name="read_more_in_github_with_link"><![CDATA[Lue lisää <font color="#0088ff">GitHub-arkistostamme</font>.]]></string> <string name="onboarding_notifications_mode_periodic">Säännölliset</string> <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty <b>2-kerroksisella päästä päähän -salauksella</b>.]]></string> - <string name="read_more_in_github">Lue lisää GitHub-arkistostamme.</string> + <string name="read_more_in_github">Lue lisää GitHub-tietovarastostamme.</string> <string name="paste_the_link_you_received">Liitä vastaanotettu linkki</string> <string name="relay_server_protects_ip">Välityspalvelin suojaa IP-osoitteesi, mutta se voi tarkkailla puhelun kestoa.</string> <string name="open_simplex_chat_to_accept_call">Avaa SimpleX Chat hyväksyäksesi puhelun</string> @@ -875,7 +872,8 @@ <string name="network_use_onion_hosts_prefer_desc">Onion-isäntiä käytetään, kun niitä on saatavilla.</string> <string name="network_use_onion_hosts_no_desc">Onion-isäntiä ei käytetä.</string> <string name="network_use_onion_hosts_required">Pakollinen</string> - <string name="network_use_onion_hosts_required_desc">Yhteyden muodostamiseen tarvitaan Onion-isäntiä.</string> + <string name="network_use_onion_hosts_required_desc">Yhteyden muodostamiseen tarvitaan Onion-isäntiä. +\nHuomioi: et voi muodostaa yhteyttä palvelimiin ilman .onion-osoitetta.</string> <string name="callstate_received_answer">vastaus saatu…</string> <string name="icon_descr_call_rejected">Hylätty puhelu</string> <string name="v4_6_chinese_spanish_interface_descr">Kiitos käyttäjille – osallistu Weblaten kautta!</string> @@ -967,7 +965,6 @@ <string name="only_you_can_make_calls">Vain sinä voit soittaa puheluita.</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä.</string> <string name="network_and_servers">Verkko &; palvelimet</string> - <string name="paste_connection_link_below_to_connect">Liitä saamasi linkki alla olevaan kenttään yhteyden muodostamiseksi kontaktiisi.</string> <string name="image_descr_qr_code">QR-koodi</string> <string name="scan_code">Skannaa koodi</string> <string name="callstate_starting">alkaa…</string> @@ -1011,7 +1008,7 @@ <string name="prohibit_message_reactions">Estä viestireaktiot.</string> <string name="prohibit_sending_disappearing_messages">Estä katoavien viestien lähettäminen.</string> <string name="accept_feature_set_1_day">Aseta 1 päivä</string> - <string name="only_your_contact_can_make_calls">Vain yhteyshenkilösi voi soittaa puheluita.</string> + <string name="only_your_contact_can_make_calls">Vain kontaktisi voi soittaa puheluita.</string> <string name="prohibit_calls">Estä ääni- ja videopuhelut.</string> <string name="prohibit_direct_messages">Estä suorien viestien lähettäminen jäsenille.</string> <string name="message_reactions_prohibited_in_this_chat">Viestireaktiot ovat kiellettyjä tässä keskustelussa.</string> @@ -1047,7 +1044,7 @@ <string name="xftp_servers">XFTP-palvelimet</string> <string name="smp_servers_your_server">Palvelimesi</string> <string name="smp_servers_your_server_address">Palvelimesi osoite</string> - <string name="enable_automatic_deletion_message">Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Se voi kestää useita minuutteja.</string> + <string name="enable_automatic_deletion_message">Tätä toimintoa ei voi kumota - valittua aikaisemmin lähetetyt ja vastaanotetut viestit poistetaan. Tämä voi kestää useita minuutteja.</string> <string name="whats_new">Uusimmat</string> <string name="you_will_still_receive_calls_and_ntfs">Saat edelleen puheluita ja ilmoituksia mykistetyiltä profiileilta, kun ne ovat aktiivisia.</string> <string name="network_disable_socks">Käytä suoraa Internet-yhteyttä\?</string> @@ -1175,7 +1172,6 @@ <string name="alert_message_no_group">Tätä ryhmää ei enää ole olemassa.</string> <string name="unhide_chat_profile">Näytä keskusteluprofiili</string> <string name="unhide_profile">Näytä profiili</string> - <string name="incognito_info_find">Löydät inkognito-yhteydessä käytetyn profiilin napauttamalla kontaktin tai ryhmän nimeä keskustelun yläreunassa.</string> <string name="chat_preferences_you_allow">Sallit</string> <string name="your_preferences">Asetuksesi</string> <string name="video_descr">Video</string> @@ -1204,7 +1200,7 @@ <string name="videos_limit_title">Liikaa videoita!</string> <string name="voice_message">Ääniviesti</string> <string name="waiting_for_video">Odottaa videota</string> - <string name="switch_receiving_address_desc">Tämä ominaisuus on kokeellinen! Se toimii vain, jos toisella on asennettuna versio 4.2. Sinun pitäisi nähdä viesti keskustelussa, kun osoitteenmuutos on valmis - tarkista, että voit edelleen vastaanottaa viestejä kyseiseltä kontaktilta (tai ryhmän jäseneltä).</string> + <string name="switch_receiving_address_desc">Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon.</string> <string name="you_need_to_allow_to_send_voice">Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä.</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Voit <font color="#0088ff">olla yhteydessä SimpleX Chatin -kehittäjiin kysyäksesi kysymyksiä ja saadaksesi päivityksiä</font>.]]></string> <string name="this_QR_code_is_not_a_link">Tämä QR-koodi ei ole linkki!</string> @@ -1232,11 +1228,10 @@ <string name="group_preview_you_are_invited">sinut on kutsuttu ryhmään</string> <string name="unmute_chat">Poista mykistys</string> <string name="you_accepted_connection">Hyväksyit yhteyden</string> - <string name="you_invited_your_contact">Kutsuit kontaktisi</string> + <string name="you_invited_a_contact">Kutsuit kontaktisi</string> <string name="call_connection_via_relay">releellä</string> <string name="database_downgrade_warning">Varoitus: saatat menettää joitain tietoja!</string> <string name="icon_descr_address">SimpleX Osoite</string> - <string name="your_profile_will_be_sent">Keskusteluprofiilisi lähetetään kontakteillesi</string> <string name="callstate_waiting_for_answer">odottaa vastaamista…</string> <string name="next_generation_of_private_messaging">Seuraavan sukupolven yksityisviestit</string> <string name="callstate_waiting_for_confirmation">odottaa vahvistusta…</string> @@ -1266,4 +1261,132 @@ <string name="settings_restart_app">Käynnistä uudelleen</string> <string name="shutdown_alert_question">Sulje\?</string> <string name="la_mode_off">Pois</string> + <string name="v5_2_message_delivery_receipts">Viestien toimituskuittaukset!</string> + <string name="v5_2_more_things_descr">- vakaampi viestien toimitus. +\n- hieman paremmat ryhmät. +\n- ja paljon muuta!</string> + <string name="delivery_receipts_are_disabled">Toimituskuittaukset poissa käytöstä!</string> + <string name="you_can_enable_delivery_receipts_later">Voit ottaa käyttöön myöhemmin asetusten kautta</string> + <string name="you_can_enable_delivery_receipts_later_alert">Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista.</string> + <string name="error_aborting_address_change">Virhe osoitteenmuutoksen keskeytyksessä</string> + <string name="abort_switch_receiving_address">Keskeytä osoitteenvaihto</string> + <string name="network_option_protocol_timeout_per_kb">Protokollan aikakatkaisu per KB</string> + <string name="files_are_prohibited_in_group">Tiedostot ja media ovat tässä ryhmässä kiellettyjä.</string> + <string name="group_members_can_send_files">Ryhmän jäsenet voivat lähettää tiedostoja ja mediaa.</string> + <string name="connect_via_link_incognito">Yhdistä Incognito</string> + <string name="connect_use_current_profile">Käytä nykyistä profiilia</string> + <string name="connect_use_new_incognito_profile">Käytä uutta incognito-profiilia</string> + <string name="turn_off_battery_optimization_button">Salli</string> + <string name="disable_notifications_button">Poista ilmoitukset käytöstä</string> + <string name="turn_off_system_restriction_button">Avaa asetukset</string> + <string name="system_restricted_background_desc">SimpleX ei toimi tausta-ajossa. Saat ilmoitukset ainostaan, kun sovellus on käynnissä.</string> + <string name="system_restricted_background_warn"><![CDATA[Ilmoitusten sallimiseksi valitse <b> Sovelluksen akun käyttö </b> / <b> rajoittamaton </b> sovellusasetuksista.]]></string> + <string name="system_restricted_background_in_call_title">Ei taustapuheluita</string> + <string name="system_restricted_background_in_call_desc">Sovellus voi sulkeutua 1 minuutin jälkeen tausta-ajossa.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Puheluiden soittamiseksi taustalla, valitse <b>Sovelluksen akun käyttö </b> / <b> rajoittamaton </b> sovellusasetuksista.]]></string> + <string name="in_reply_to">Vastauksena</string> + <string name="abort_switch_receiving_address_question">Keskeytä osoitteenvaihto\?</string> + <string name="favorite_chat">Suosikki</string> + <string name="unfavorite_chat">Epäsuosikki</string> + <string name="connect__a_new_random_profile_will_be_shared">Uusi satunnainen profiili jaetaan.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Liitä linkki, jonka sait yhteydenottoon kontaktisi kanssa…</string> + <string name="connect__your_profile_will_be_shared">Profiilisi %1$s jaetaan.</string> + <string name="receipts_groups_title_disable">Kuittaukset pois käytöstä ryhmiltä\?</string> + <string name="in_developing_title">Tulossa pian!</string> + <string name="receipts_groups_disable_for_all">Poista käytöstä kaikilta ryhmiltä</string> + <string name="files_and_media">Tiedostot ja media</string> + <string name="receipts_groups_enable_keep_overrides">Salli (pidä ryhmäohitukset)</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">"Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille näkyvissä keskusteluprofiileissa."</string> + <string name="receipts_groups_override_enabled">Kuittauksien lähettäminen on käytössä %d ryhmille</string> + <string name="abort_switch_receiving_address_confirm">Keskeytä</string> + <string name="sync_connection_force_confirm">Uudelleenneuvottele</string> + <string name="sync_connection_force_question">Uudelleenneuvottele salaus\?</string> + <string name="sync_connection_force_desc">Salaus toimii ja uutta salaussopimusta ei tarvita. Tämä voi johtaa yhteysvirheisiin!</string> + <string name="receipts_section_description">Nämä asetukset koskevat nykyistä profiiliasi</string> + <string name="receipts_section_description_1">Ne voidaan ohittaa kontakti- ja ryhmäasetuksissa.</string> + <string name="conn_event_ratchet_sync_ok">salaus ok</string> + <string name="conn_event_ratchet_sync_allowed">salauksen uudelleenneuvottelu sallittu</string> + <string name="fix_connection_confirm">Korjaa</string> + <string name="fix_connection_not_supported_by_contact">Kontakti ei tue korjausta</string> + <string name="fix_connection_not_supported_by_group_member">Ryhmän jäsen ei tue korjausta</string> + <string name="renegotiate_encryption">Uudelleenneuvottele salaus</string> + <string name="in_developing_desc">Tätä ominaisuutta ei vielä tueta. Kokeile seuraavaa versiota.</string> + <string name="delivery">Toimitus</string> + <string name="no_info_on_delivery">Ei toimitustietoja</string> + <string name="no_filtered_chats">Ei suodatettuja keskusteluja</string> + <string name="no_selected_chat">Ei valittua keskustelua</string> + <string name="only_owners_can_enable_files_and_media">Vain ryhmän omistajat voivat sallia tiedostoja ja mediaa.</string> + <string name="files_and_media_prohibited">Tiedostot ja media kielletty!</string> + <string name="abort_switch_receiving_address_desc">Osoitteenmuutos keskeytetään. Käytetään vanhaa vastaanotto-osoitetta.</string> + <string name="choose_file_title">Valitse tiedosto</string> + <string name="receipts_section_groups">Pienryhmät (max 20)</string> + <string name="send_receipts_disabled_alert_title">Kuittaukset pois käytöstä</string> + <string name="send_receipts_disabled_alert_msg">Ryhmällä on yli %1$d jäsentä, toimituskuittauksia ei lähetetä.</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="v5_2_more_things">Muutama asia lisää</string> + <string name="v5_2_fix_encryption">Pidä kontaktisi</string> + <string name="receipts_section_contacts">Kontaktit</string> + <string name="receipts_contacts_disable_for_all">Poista käytöstä kaikilta</string> + <string name="receipts_contacts_enable_for_all">Salli kaikille</string> + <string name="receipts_contacts_enable_keep_overrides">Salli (pidä ohitukset)</string> + <string name="receipts_contacts_disable_keep_overrides">Poista käytöstä (pidä ohitukset)</string> + <string name="receipts_contacts_title_disable">Kuittaukset pois käytöstä\?</string> + <string name="receipts_contacts_title_enable">Salli kuittaukset\?</string> + <string name="receipts_contacts_override_disabled">Kuittauksien lähettäminen on pois käytöstä %d kontakteilta</string> + <string name="receipts_contacts_override_enabled">Kuittauksien lähettäminen on käytössä %d kontakteille</string> + <string name="settings_section_title_delivery_receipts">LÄHETÄ TOIMITUSKUITTAUKSET VASTAANOTTAJALLE</string> + <string name="rcv_conn_event_verification_code_reset">turvakoodi on muuttunut</string> + <string name="conn_event_ratchet_sync_started">hyväksyy salausta…</string> + <string name="snd_conn_event_ratchet_sync_allowed">salauksen uudelleenneuvottelu sallittu %s:lle</string> + <string name="conn_event_ratchet_sync_required">tarvitaan salauksen uudelleenneuvottelua</string> + <string name="snd_conn_event_ratchet_sync_started">hyväksyy salausta %s:lle…</string> + <string name="conn_event_ratchet_sync_agreed">salaus sovittu</string> + <string name="snd_conn_event_ratchet_sync_agreed">salaus sovittu %s:lle</string> + <string name="snd_conn_event_ratchet_sync_ok">salaus ok %s:lle</string> + <string name="snd_conn_event_ratchet_sync_required">tarvitaan salauksen uudelleenneuvottelua %s:lle</string> + <string name="sender_at_ts">"%s klo %s"</string> + <string name="send_receipts">Lähetä kuittaukset</string> + <string name="fix_connection">Korjaa yhteys</string> + <string name="fix_connection_question">Korjaa yhteys\?</string> + <string name="allow_to_send_files">Salli tiedostojen ja median lähettäminen.</string> + <string name="prohibit_sending_files">Estä tiedostojen ja median lähettäminen.</string> + <string name="v5_2_disappear_one_message">Hävitä yksi viesti</string> + <string name="v5_2_fix_encryption_descr">Korjaa salaus varmuuskopioiden palauttamisen jälkeen.</string> + <string name="v5_2_disappear_one_message_descr">Jopa kun ei käytössä keskustelussa.</string> + <string name="receipts_groups_disable_keep_overrides">Poista käytöstä (pidä ryhmäohitukset)</string> + <string name="receipts_groups_enable_for_all">Salli kaikille ryhmille</string> + <string name="receipts_groups_title_enable">Salli kuittaukset ryhmille\?</string> + <string name="privacy_message_draft">Viestiluonnos</string> + <string name="receipts_groups_override_disabled">Kuittauksien lähettäminen on pois käytöstä %d ryhmiltä</string> + <string name="privacy_show_last_messages">Näytä viimeiset viestit</string> + <string name="send_receipts_disabled">ei käytössä</string> + <string name="rcv_group_event_2_members_connected">%s ja %s yhdistetty</string> + <string name="rcv_group_event_n_members_connected">%s, %s ja %d muut jäsenet yhdistetty</string> + <string name="rcv_group_event_3_members_connected">%s, %s ja %s yhdistetty</string> + <string name="dont_enable_receipts">Älä salli</string> + <string name="error_enabling_delivery_receipts">Virhe toimituskuittauksien sallimisessa!</string> + <string name="connect_via_member_address_alert_title">Yhdistä suoraan\?</string> + <string name="connect_via_member_address_alert_desc">Yhteyspyyntö lähetetään tälle ryhmän jäsenelle.</string> + <string name="delivery_receipts_title">Toimituskuittaukset!</string> + <string name="enable_receipts_all">Salli</string> + <string name="v5_2_favourites_filter_descr">Suodata lukemattomia- ja suosikkikeskusteluja.</string> + <string name="v5_2_favourites_filter">Löydä keskustelut nopeammin</string> + <string name="sending_delivery_receipts_will_be_enabled">Toimituskuittauksien lähettäminen otetaan käyttöön kaikille kontakteille.</string> + <string name="v5_2_message_delivery_receipts_descr">Toinen kuittaus, joka uupui! ✅</string> + <string name="error_synchronizing_connection">Virhe yhteyden synkronoinnissa</string> + <string name="no_history">Ei historiaa</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Tietokanta salataan ja tunnuslause tallennetaan asetuksiin.</string> + <string name="you_can_change_it_later">Satunnainen tunnuslause on tallennettu asetuksiin selkokielisenä. +\nVoit muuttaa sen myöhemmin.</string> + <string name="database_encryption_will_be_updated_in_settings">Tietokannan salaustunnuslause päivitetään ja tallennetaan asetuksiin.</string> + <string name="remove_passphrase_from_settings">Poista tunnuslause asetuksista\?</string> + <string name="use_random_passphrase">Käytä satunnaista tunnuslausetta</string> + <string name="save_passphrase_in_settings">Tallenna tunnuslause asetuksiin</string> + <string name="setup_database_passphrase">Aseta tietokannan tunnuslause</string> + <string name="set_database_passphrase">Aseta tietokannan tunnuslause</string> + <string name="open_database_folder">Avaa tietokantakansio</string> + <string name="passphrase_will_be_saved_in_settings">Tunnuslause tallennetaan asetuksiin selkokielisenä sen jälkeen, kun olet vaihtanut sen tai käynnistänyt sovelluksen uudelleen.</string> + <string name="settings_is_storing_in_clear_text">Tunnuslause on tallennettu asetuksiin selkokielisenä.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b> Huomioi </b>: Viesti- ja tiedostovälittimet yhdistetään SOCKS-proxyn kautta. Puhelut ja linkin esikatselut käyttävät suoraa yhteyttä.]]></string> + <string name="encrypt_local_files">Salaa paikalliset tiedostot</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fonts/NotoColorEmoji-Regular.ttf b/apps/multiplatform/common/src/commonMain/resources/MR/fonts/NotoColorEmoji-Regular.ttf new file mode 100644 index 0000000000..42799e84e8 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/fonts/NotoColorEmoji-Regular.ttf differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 1462e04f61..9ac692cf70 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -84,7 +84,7 @@ <string name="periodic_notifications">Notifications périodiques</string> <string name="periodic_notifications_disabled">Les notifications périodiques sont désactivées !</string> <string name="enter_passphrase_notification_title">Une phrase secrète est nécessaire</string> - <string name="turn_off_battery_optimization"><![CDATA[Pour l\'utiliser, veuillez <b>désactiver l\'optimisation de la batterie</b> pour SimpleX dans la prochaine fenêtre de dialogue. Sinon, les notifications seront désactivées.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Pour l\'utiliser, veuillez <b>autoriser SimpleX à fonctionner en arrière-plan</b> dans la fenêtre de dialogue suivante. Sinon, les notifications seront désactivées.]]></string> <string name="error_smp_test_server_auth">Le serveur requiert une autorisation pour créer des files d\'attente, vérifiez le mot de passe</string> <string name="periodic_notifications_desc">L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs.</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Pour protéger votre vie privée, au lieu des notifications push, l\'application possède un <b>SimpleX service de fond</b> - il utilise quelques pour cent de la batterie par jour.]]></string> @@ -221,7 +221,7 @@ <string name="mark_read">Marquer comme lu</string> <string name="mark_unread">Marquer non lu</string> <string name="set_contact_name">Définir le nom du contact</string> - <string name="you_invited_your_contact">Vous avez invité votre contact</string> + <string name="you_invited_a_contact">Vous avez invité votre contact</string> <string name="you_accepted_connection">Vous avez accepté la connexion</string> <string name="delete_pending_connection__question">Supprimer la connexion en attente \?</string> <string name="connection_you_accepted_will_be_cancelled">La connexion que vous avez acceptée sera annulée !</string> @@ -243,7 +243,6 @@ <string name="your_chat_profile_will_be_sent_to_your_contact">Votre profil de chat sera envoyé \nà votre contact</string> <string name="share_invitation_link">Partager un lien unique</string> - <string name="your_profile_will_be_sent">Votre profil de chat sera envoyé à votre contact</string> <string name="paste_button">Coller</string> <string name="this_string_is_not_a_connection_link">Cette chaîne n\'est pas un lien de connexion !</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Vous pouvez aussi vous connecter en cliquant sur le lien. Si il s\'ouvre dans le navigateur, cliquez sur <b>Ouvrir dans l\'app mobile</b>.]]></string> @@ -254,8 +253,7 @@ <string name="icon_descr_server_status_pending">En attente</string> <string name="accept_connection_request__question">Accepter la demande de connexion \?</string> <string name="clear_verb">Effacer</string> - <string name="clear_chat_button">Effacer la conversation</string> - <string name="paste_connection_link_below_to_connect">Collez le lien que vous avez reçu dans le cadre ci-dessous pour vous connecter avec votre contact.</string> + <string name="clear_chat_button">Effacer le chat</string> <string name="connect_via_link">Se connecter via un lien</string> <string name="clear_verification">Retirer la vérification</string> <string name="one_time_link">Lien d\'invitation unique</string> @@ -423,7 +421,8 @@ <string name="update_onion_hosts_settings_question">Mettre à jour le paramètre des hôtes .onion \?</string> <string name="network_use_onion_hosts_prefer">Quand disponible</string> <string name="network_use_onion_hosts_no_desc">Les hôtes .onion ne seront pas utilisés.</string> - <string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion.</string> + <string name="network_use_onion_hosts_required_desc">Les hôtes .onion seront nécessaires pour la connexion. +\nAttention : vous ne pourrez pas vous connecter aux serveurs sans adresse .onion.</string> <string name="network_use_onion_hosts_no_desc_in_alert">Les hôtes .onion ne seront pas utilisés.</string> <string name="delete_address__question">Supprimer l\'adresse \?</string> <string name="all_your_contacts_will_remain_connected">Tous vos contacts resteront connectés.</string> @@ -464,7 +463,7 @@ <string name="read_more_in_github_with_link"><![CDATA[Pour en savoir plus, consultez notre <font color="#0088ff">GitHub repository</font>.]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Batterie peu utilisée</b>. Le service de fond vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Batterie plus utilisée </b> ! Le service de fond est toujours en cours d\'exécution - les notifications s\'affichent dès que les messages sont disponibles.]]></string> - <string name="integrity_msg_skipped">%1$d message⸱s manqué⸱s</string> + <string name="integrity_msg_skipped">%1$d message(s) manqué(s)</string> <string name="integrity_msg_bad_id">ID de message incorrecte</string> <string name="settings_section_title_settings">PARAMÈTRES</string> <string name="alert_text_skipped_messages_it_can_happen_when">Cela peut arriver quand : @@ -784,10 +783,7 @@ <string name="update_network_settings_question">Mettre à jour les paramètres réseau \?</string> <string name="incognito">Incognito</string> <string name="incognito_random_profile">Votre profil aléatoire</string> - <string name="incognito_random_profile_description">Un profil aléatoire sera envoyé à votre contact</string> - <string name="incognito_random_profile_from_contact_description">Un profil aléatoire sera envoyé au contact qui vous a envoyé ce lien</string> <string name="incognito_info_allows">Cela permet d\'avoir plusieurs connections anonymes sans aucune données partagées entre elles sur un même profil.</string> - <string name="incognito_info_find">Pour trouver le profil utilisé lors d\'une connexion incognito, appuyez sur le nom du contact ou du groupe en haut du chat.</string> <string name="theme_light">Clair</string> <string name="theme_dark">Sombre</string> <string name="theme">Thème</string> @@ -804,7 +800,7 @@ <string name="group_preferences">Préférences du groupe</string> <string name="set_group_preferences">Définir les préférences du groupe</string> <string name="your_preferences">Vos préférences</string> - <string name="allow_your_contacts_irreversibly_delete">Autorise votre contact à supprimer de façon définitive des messages envoyés.</string> + <string name="allow_your_contacts_irreversibly_delete">Autorise vos contacts à supprimer de manière irréversible les messages envoyés.</string> <string name="contacts_can_mark_messages_for_deletion">Vos contacts peuvent marquer les messages pour les supprimer ; vous pourrez les consulter.</string> <string name="allow_your_contacts_to_send_voice_messages">Autorise vos contacts à envoyer des messages vocaux.</string> <string name="allow_voice_messages_only_if">Autoriser les messages vocaux uniquement si votre contact les autorise.</string> @@ -820,7 +816,7 @@ <string name="ttl_min">%d min</string> <string name="ttl_month">%d mois</string> <string name="ttl_months">%d mois</string> - <string name="ttl_m">%dm</string> + <string name="ttl_m">%dmn</string> <string name="ttl_mth">%dm</string> <string name="ttl_hour">%d heure</string> <string name="ttl_hours">%d heures</string> @@ -844,7 +840,7 @@ <string name="group_members_can_send_disappearing">Les membres du groupes peuvent envoyer des messages éphémères.</string> <string name="network_options_revert">Revenir en arrière</string> <string name="prohibit_sending_disappearing_messages">Interdire l’envoi de messages éphémères.</string> - <string name="incognito_info_protects">Le mode Incognito protège la confidentialité de votre profil principal — pour chaque nouveau contact un nouveau profil aléatoire est créé.</string> + <string name="incognito_info_protects">Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact.</string> <string name="updating_settings_will_reconnect_client_to_all_servers">La mise à jour des ces paramètres reconnectera le client à tous les serveurs.</string> <string name="incognito_info_share">Lorsque vous partagez un profil incognito avec quelqu\'un, ce profil sera utilisé pour les groupes auxquels il vous invite.</string> <string name="chat_preferences_yes">oui</string> @@ -855,7 +851,6 @@ <string name="both_you_and_your_contact_can_send_disappearing">Vous et votre contact êtes tous deux en mesure d\'envoyer des messages éphémères.</string> <string name="voice_messages_are_prohibited">Les messages vocaux sont interdits dans ce groupe.</string> <string name="group_display_name_field">Nom affiché du groupe :</string> - <string name="group_unsupported_incognito_main_profile_sent">Le mode Incognito n\'est pas supporté ici - votre profil principal sera envoyé aux membres du groupe</string> <string name="conn_level_desc_indirect">indirecte (%1$s)</string> <string name="info_row_group">Groupe</string> <string name="info_row_connection">Connexion</string> @@ -910,7 +905,7 @@ <string name="feature_offered_item">offert %s</string> <string name="feature_offered_item_with_param">offert %s: %2s</string> <string name="feature_cancelled_item">annulé %s</string> - <string name="app_version_title">Version de l\'application</string> + <string name="app_version_title">Version de l\'app</string> <string name="core_simplexmq_version">simplexmq : v%s (%2s)</string> <string name="app_version_code">Build de l\'app : %s</string> <string name="app_version_name">Version de l\'app : v%s</string> @@ -928,8 +923,8 @@ <string name="network_session_mode_entity">Connexion</string> <string name="delete_files_and_media_for_all_users">Effacer les fichiers de tous les profils de chat</string> <string name="users_delete_with_connections">Profil et connexions au serveur</string> - <string name="network_session_mode_transport_isolation">Isolement du transport</string> - <string name="update_network_session_mode_question">Mettre à jour le mode d\'isolation du transport \?</string> + <string name="network_session_mode_transport_isolation">Transport isolé</string> + <string name="update_network_session_mode_question">Mettre à jour le mode d\'isolement du transport \?</string> <string name="network_session_mode_entity_description">Une connexion TCP distincte (et identifiant SOCKS) sera utilisée <b>pour chaque contact et membre de groupe</b>. \n<b>Veuillez noter</b> : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer.</string> <string name="network_session_mode_user">Profil de chat</string> @@ -953,8 +948,8 @@ <string name="v4_5_italian_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string> <string name="v4_4_french_interface_descr">Merci aux utilisateurs - contribuez via Weblate !</string> <string name="v4_5_private_filenames_descr">Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC.</string> - <string name="v4_5_transport_isolation">Isolation du transport</string> - <string name="v4_5_multiple_chat_profiles_descr">Différents noms, avatars et mode d\'isolation de transport.</string> + <string name="v4_5_transport_isolation">Transport isolé</string> + <string name="v4_5_multiple_chat_profiles_descr">Différents noms, avatars et modes d\'isolement de transport.</string> <string name="moderated_description">modéré</string> <string name="moderated_item_description">modéré par %s</string> <string name="delete_member_message__question">Supprimer le message de ce membre \?</string> @@ -996,7 +991,7 @@ <string name="make_profile_private">Rendre un profil privé !</string> <string name="user_mute">Mute</string> <string name="v4_6_hidden_chat_profiles_descr">Protégez vos profils de chat par un mot de passe !</string> - <string name="tap_to_activate_profile">Appuyez pour activer le profil.</string> + <string name="tap_to_activate_profile">Appuyez pour activer un profil.</string> <string name="save_and_update_group_profile">Sauvegarder et mettre à jour le profil du groupe</string> <string name="save_profile_password">Enregistrer le mot de passe du profil</string> <string name="to_reveal_profile_enter_password">Pour révéler votre profil caché, entrez le mot de passe dans le champ de recherche de la page Profils de chat.</string> @@ -1025,7 +1020,7 @@ <string name="show_dev_options">Afficher :</string> <string name="show_developer_options">Afficher les options pour les développeurs</string> <string name="file_will_be_received_when_contact_completes_uploading">Le fichier sera reçu lorsque votre contact aura terminé de le mettre en ligne.</string> - <string name="developer_options">IDs de base de données et option d\'isolation du transport.</string> + <string name="developer_options">IDs de base de données et option d\'isolement du transport.</string> <string name="settings_section_title_experimenta">EXPÉRIMENTALE</string> <string name="hide_dev_options">Cacher :</string> <string name="unhide_chat_profile">Dévoiler le profil de chat</string> @@ -1107,7 +1102,7 @@ <string name="submit_passcode">Soumettre</string> <string name="both_you_and_your_contact_can_make_calls">Vous et votre contact pouvez tous deux passer des appels.</string> <string name="only_you_can_make_calls">Vous seul pouvez passer des appels.</string> - <string name="calls_prohibited_with_this_contact">Interdire les appels audio/vidéo.</string> + <string name="calls_prohibited_with_this_contact">Les appels audio/vidéo sont interdits.</string> <string name="confirm_passcode">Confirmer le code d\'accès</string> <string name="lock_after">Verrouillage après</string> <string name="lock_mode">Mode de verrouillage</string> @@ -1163,7 +1158,7 @@ <string name="add_address_to_your_profile">Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d\'autres personnes. La mise à jour du profil sera envoyée à vos contacts.</string> <string name="color_secondary_variant">Secondaire supplémentaire</string> <string name="all_your_contacts_will_remain_connected_update_sent">Tous vos contacts resteront connectés. La mise à jour du profil sera envoyée à vos contacts.</string> - <string name="auto_accept_contact">Auto-acceptation</string> + <string name="auto_accept_contact">Auto-accepter</string> <string name="create_simplex_address">Créer une adresse SimpleX</string> <string name="customize_theme_title">Personnaliser le thème</string> <string name="continue_to_next_step">Continuer</string> @@ -1227,7 +1222,7 @@ <string name="error_loading_details">Erreur de chargement des détails</string> <string name="edit_history">Historique</string> <string name="info_menu">Info</string> - <string name="search_verb">Rechercher</string> + <string name="search_verb">Recherche</string> <string name="received_message">Message reçu</string> <string name="custom_time_picker_custom">personnalisé</string> <string name="custom_time_picker_select">Choisir</string> @@ -1314,7 +1309,7 @@ <string name="sync_connection_force_confirm">Renégocier</string> <string name="rcv_conn_event_verification_code_reset">code de sécurité modifié</string> <string name="sending_delivery_receipts_will_be_enabled_all_profiles">L\'envoi d\'accusés de réception sera activé pour tous les contacts dans tous les profils de chat visibles.</string> - <string name="receipts_section_description_1">Ils peuvent être remplacés dans les paramètres des contacts</string> + <string name="receipts_section_description_1">Ils peuvent être modifiés dans les paramètres des contacts et des groupes.</string> <string name="receipts_section_description">Ces paramètres s\'appliquent à votre profil actuel</string> <string name="you_can_enable_delivery_receipts_later_alert">Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l\'application.</string> <string name="receipts_contacts_title_enable">Activer les accusés de réception \?</string> @@ -1322,7 +1317,7 @@ <string name="receipts_contacts_override_enabled">L\'envoi d\'accusés de réception est activé pour les contacts de %d</string> <string name="conn_event_ratchet_sync_started">accord sur le chiffrement…</string> <string name="conn_event_ratchet_sync_agreed">chiffrement accepté</string> - <string name="conn_event_ratchet_sync_ok">chiffrement ok</string> + <string name="conn_event_ratchet_sync_ok">chiffrement OK</string> <string name="conn_event_ratchet_sync_allowed">renégociation de chiffrement autorisée</string> <string name="conn_event_ratchet_sync_required">renégociation de chiffrement requise</string> <string name="snd_conn_event_ratchet_sync_ok">chiffrement ok pour %s</string> @@ -1342,4 +1337,70 @@ <string name="error_synchronizing_connection">Erreur de synchronisation de connexion</string> <string name="no_history">Aucun historique</string> <string name="sync_connection_force_question">Renégocier le chiffrement\?</string> + <string name="choose_file_title">Choisissez un fichier</string> + <string name="in_developing_title">Bientôt disponible !</string> + <string name="receipts_section_groups">Petits groupes (max 20)</string> + <string name="receipts_groups_disable_keep_overrides">Désactivé (conserver la priorité sur les groupes)</string> + <string name="receipts_groups_title_disable">Désactiver les reçus pour les groupes \?</string> + <string name="receipts_groups_enable_for_all">Activé pour tous les groupes</string> + <string name="receipts_groups_enable_keep_overrides">Activé (conserver la priorité sur les groupes)</string> + <string name="receipts_groups_override_disabled">L\'envoi de reçus est désactivé pour %d groupes</string> + <string name="receipts_groups_override_enabled">L\'envoi de reçus est activé pour %d groupes</string> + <string name="receipts_groups_disable_for_all">Désactivé pour tous les groupes</string> + <string name="send_receipts_disabled_alert_title">Les accusés de réception sont désactivés</string> + <string name="in_developing_desc">Cette fonctionnalité n\'est pas encore prise en charge. Essayez la dans la prochaine version.</string> + <string name="delivery">Distribution</string> + <string name="send_receipts_disabled">désactivé</string> + <string name="no_info_on_delivery">Pas d\'information sur la distribution</string> + <string name="no_selected_chat">Aucun chat sélectionné</string> + <string name="receipts_groups_title_enable">Activer les reçus pour les groupes \?</string> + <string name="send_receipts_disabled_alert_msg">Ce groupe compte plus de %1$d membres, les accusés de réception ne sont pas envoyés.</string> + <string name="recipient_colon_delivery_status">%s : %s</string> + <string name="connect_via_member_address_alert_title">Connexion directe \?</string> + <string name="connect_via_member_address_alert_desc">Une demande de connexion sera envoyée à ce membre du groupe.</string> + <string name="turn_off_battery_optimization_button">Autoriser</string> + <string name="connect__a_new_random_profile_will_be_shared">Un nouveau profil aléatoire sera partagé.</string> + <string name="disable_notifications_button">Désactiver les notifications</string> + <string name="system_restricted_background_warn"><![CDATA[Pour activer les notifications, veuillez choisir <b>Utilisation de la batterie de l\'app</b> / <b>Sans restriction</b> dans les paramètres de l\'app.]]></string> + <string name="connect__your_profile_will_be_shared">Votre profil %1$s sera partagé.</string> + <string name="connect_via_link_incognito">Se connecter incognito</string> + <string name="system_restricted_background_in_call_title">Pas d\'appels en arrière-plan</string> + <string name="turn_off_system_restriction_button">Ouvrir les paramètres de l\'app</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Collez le lien que vous avez reçu pour vous connecter à votre contact…</string> + <string name="system_restricted_background_desc">SimpleX ne peut pas fonctionner en arrière-plan. Vous ne recevrez les notifications que lorsque l\'application sera en cours d\'exécution.</string> + <string name="system_restricted_background_in_call_desc">L\'application peut se fermer après 1 minute en arrière-plan.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Pour faire des appels en arrière-plan, veuillez choisir <b>Utilisation de la batterie de l\'app</b> / <b>Sans restriction</b> dans les paramètres de l\'app.]]></string> + <string name="connect_use_current_profile">Utiliser le profil actuel</string> + <string name="connect_use_new_incognito_profile">Utiliser un nouveau profil incognito</string> + <string name="privacy_message_draft">Brouillon de message</string> + <string name="privacy_show_last_messages">Voir les derniers messages</string> + <string name="rcv_group_event_2_members_connected">%s et %s sont connecté.es</string> + <string name="rcv_group_event_n_members_connected">%s, %s et %d autres membres sont connectés</string> + <string name="rcv_group_event_3_members_connected">%s, %s et %s sont connecté.es</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">La base de données sera chiffrée et la phrase de passe sera stockée dans les paramètres.</string> + <string name="you_can_change_it_later">La phrase secrète aléatoire est stockée en clair dans les paramètres. +\nVous pouvez la modifier ultérieurement.</string> + <string name="database_encryption_will_be_updated_in_settings">La phrase de chiffrement de la base de données sera mise à jour et stockée dans les paramètres.</string> + <string name="remove_passphrase_from_settings">Supprimer la phrase secrète des paramètres \?</string> + <string name="use_random_passphrase">Utiliser une phrase secrète aléatoire</string> + <string name="save_passphrase_in_settings">Enregistrer la phrase secrète dans les paramètres</string> + <string name="setup_database_passphrase">Configurer la phrase secrète de la base de données</string> + <string name="set_database_passphrase">Définir la phrase secrète de la base de données</string> + <string name="open_database_folder">Ouvrir le dossier de la base de données</string> + <string name="passphrase_will_be_saved_in_settings">La phrase secrète sera stockée en clair dans les paramètres après que vous la modifiez ou que vous redémarrez l\'application.</string> + <string name="settings_is_storing_in_clear_text">La phrase secrète est stockée en clair dans les paramètres.</string> + <string name="v5_3_encrypt_local_files">Chiffrement des fichiers et des médias stockés</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Remarque</b> : Les relais de messages et de fichiers sont connectés par le biais d\'un proxy SOCKS. Les appels et l\'envoi d\'aperçus de liens utilisent une connexion directe.]]></string> + <string name="encrypt_local_files">Chiffrer les fichiers locaux</string> + <string name="v5_3_new_desktop_app">Nouvelle application de bureau !</string> + <string name="v5_3_new_interface_languages">6 nouvelles langues d\'interface</string> + <string name="v5_3_encrypt_local_files_descr">L\'application chiffre les nouveaux fichiers locaux (sauf les vidéos).</string> + <string name="v5_3_discover_join_groups">Découvrir et rejoindre des groupes</string> + <string name="v5_3_simpler_incognito_mode">Mode incognito simplifié</string> + <string name="v5_3_new_interface_languages_descr">Arabe, bulgare, finnois, hébreu, thaï et ukrainien - grâce aux utilisateurs et à Weblate.</string> + <string name="v5_3_new_desktop_app_descr">Créer un nouveau profil sur l\'application de bureau. 💻</string> + <string name="v5_3_simpler_incognito_mode_descr">Basculer en mode incognito lors de la connexion.</string> + <string name="v5_3_discover_join_groups_descr">- connexion au service d\'annuaire (BETA) ! +\n- accusés de réception (jusqu\'à 20 membres). +\n- plus rapide et plus stable.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml index 049df269ea..56dfb51e2e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml @@ -253,7 +253,6 @@ <string name="v4_2_auto_accept_contact_requests">संपर्क अनुरोधों को स्वत: स्वीकार करें</string> <string name="integrity_msg_bad_hash">खराब संदेश हैश</string> <string name="allow_voice_messages_only_if">ध्वनि संदेशों को केवल तभी अनुमति दें यदि आपका संपर्क उन्हें अनुमति देता है।</string> - <string name="incognito_random_profile_from_contact_description">जिस संपर्क से आपने यह लिंक प्राप्त किया है, उसे एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string> <string name="callstatus_calling">कॉल कर रहा है…</string> <string name="feature_cancelled_item">रद्द %s</string> <string name="rcv_conn_event_switch_queue_phase_completed">आपके लिए पता बदल दिया</string> @@ -262,7 +261,6 @@ <string name="ttl_month">%d महीना</string> <string name="ttl_months">%d महीने</string> <string name="settings_developer_tools">डेवलपर उपकरण</string> - <string name="incognito_random_profile_description">आपके संपर्क को एक यादृच्छिक प्रोफ़ाइल भेजी जाएगी</string> <string name="snd_conn_event_switch_queue_phase_changing">पता बदल रहा है…</string> <string name="button_add_welcome_message">स्वागत संदेश जोड़ें</string> <string name="allow_your_contacts_irreversibly_delete">अपने संपर्कों को भेजे गए संदेशों को अपरिवर्तनीय रूप से हटाने की अनुमति दें।</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg new file mode 100644 index 0000000000..7b003c8e7c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + height="24" + viewBox="0 -960 960 960" + width="24" + version="1.1" + id="svg864" + sodipodi:docname="ic_bolt_off.svg" + inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <defs + id="defs868"> + <clipPath + clipPathUnits="userSpaceOnUse" + id="clipPath1042"> + <g + id="g1046"> + <g + inkscape:label="Clip" + id="use1044" + clip-path="url(#clipPath1042)"> + <g + id="g1054" /> + </g> + </g> + </clipPath> + </defs> + <sodipodi:namedview + id="namedview866" + pagecolor="#505050" + bordercolor="#ffffff" + borderopacity="1" + inkscape:showpageshadow="0" + inkscape:pageopacity="0" + inkscape:pagecheckerboard="1" + inkscape:deskcolor="#505050" + showgrid="false" + inkscape:zoom="5.0911688" + inkscape:cx="-25.239784" + inkscape:cy="-0.49104638" + inkscape:window-width="1440" + inkscape:window-height="856" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg864" /> + <path + id="path1067" + d="M 367.38281 -591.32812 L 326.67969 -632.65625 L 279.53125 -564.57031 L 166.67969 -401.5625 C 159.67969 -391.89551 158.77538 -382.00292 163.98438 -371.83594 C 169.19336 -361.66896 177.59871 -356.5625 189.17969 -356.5625 L 333.67188 -356.5625 L 314.6875 -222.8125 L 295.70312 -89.0625 C 294.37014 -80.895816 295.62013 -73.294223 299.45312 -66.210938 C 303.28612 -59.127653 308.83888 -53.987005 316.17188 -50.820312 C 323.50486 -47.320319 331.02496 -46.494079 338.71094 -48.359375 C 346.39692 -50.224071 353.04388 -54.490464 358.67188 -61.09375 L 482.53906 -209.72656 L 601.99219 -353.125 L 561.75781 -393.98438 L 552.69531 -383.08594 L 458.20312 -269.57031 L 363.67188 -156.09375 L 381.67969 -285.07812 L 399.6875 -414.0625 L 244.6875 -414.0625 L 330.50781 -538.08594 L 367.38281 -591.32812 z " /> + <path + id="path1128" + d="M 523.94531 -904.84375 C 516.11233 -902.34376 509.86231 -897.58593 505.19531 -890.58594 L 392.34375 -727.57812 L 371.91406 -698.04688 L 413.04688 -657.26562 L 416.36719 -662.07031 L 502.1875 -786.09375 L 483.94531 -641.32812 L 477.85156 -593.00781 L 575.15625 -496.5625 L 647.1875 -496.5625 L 614.60938 -457.46094 L 655.27344 -417.10938 L 730.19531 -507.07031 C 737.86229 -516.73729 739.14756 -526.97952 734.10156 -537.8125 C 729.05558 -548.64547 720.4441 -554.0625 708.20312 -554.0625 L 530.70312 -554.0625 L 550.42969 -709.84375 L 570.19531 -865.58594 C 571.19531 -873.35892 569.42186 -880.88407 564.92188 -888.16406 C 560.42188 -895.44405 554.52048 -900.7295 547.1875 -904.0625 C 539.52052 -907.06249 531.77829 -907.34375 523.94531 -904.84375 z " /> + <path + d="m 98.757614,-815.50208 c -6,-5.6667 -9,-12.4944 -9,-20.483 0,-7.9894 3,-14.995 9,-21.017 5.666666,-5.6667 12.499996,-8.5 20.499996,-8.5 8,0 14.87033,2.8703 20.611,8.611 l 703.77795,703.77797 c 5.7407,5.74067 8.611,12.52767 8.611,20.361 0,7.83333 -2.8333,14.58333 -8.5,20.25 -5.6666,6 -12.58329,8.91667 -20.74999,8.75 -8.1666,-0.16666 -15.0646,-3.06466 -20.694,-8.694 C 515.62153,-363.72015 368.7575,-545.07432 98.757614,-815.50208 Z" + id="path114" + sodipodi:nodetypes="cscsssscscc" /> +</svg> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg new file mode 100644 index 0000000000..132c514e92 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M241.776-244.5 134-136.5q-13.5 13.5-31.25 6.359Q85-137.281 85-156.5V-818q0-22.969 17.266-40.234Q119.531-875.5 142.5-875.5h675q22.969 0 40.234 17.266Q875-840.969 875-818v516q0 22.969-17.266 40.234Q840.469-244.5 817.5-244.5H241.776ZM142.5-302h675v-516h-675v516Zm0 0v-516 516Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg new file mode 100644 index 0000000000..e9c30f5199 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M422.5-182v-99.5h-280q-22.969 0-40.234-17.266Q85-316.031 85-339v-439q0-22.969 17.266-40.234Q119.531-835.5 142.5-835.5h675q22.969 0 40.234 17.266Q875-800.969 875-778v439q0 22.969-17.266 40.234Q840.469-281.5 817.5-281.5h-280v99.5h57q11.675 0 20.088 8.463Q623-165.074 623-153.325q0 12.325-8.412 20.575-8.413 8.25-20.088 8.25H366q-12.25 0-20.625-8.425-8.375-8.426-8.375-20.5 0-12.075 8.375-20.325T366-182h56.5Zm-280-157h675v-439h-675v439Zm0 0v-439 439Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_heart@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_heart@4x.png new file mode 100644 index 0000000000..96536ef634 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_heart@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg deleted file mode 100644 index bf6b7b47b4..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 96 960 960" width="24"><path d="M222 971q-23.719 0-40.609-16.891Q164.5 937.219 164.5 913.5v-431q0-23.719 16.891-40.609Q198.281 425 222 425h387v-95.385q0-53.782-37.373-91.198Q534.254 201 479.863 201q-46.363 0-81.363 28T354 300.5q-3 13-11.75 21.25T321.983 330q-12.311 0-20.397-8.5-8.086-8.5-6.086-20 10-68 61.902-113t122.629-45q77.383 0 131.926 54.551Q666.5 252.603 666.5 330v95H738q23.719 0 40.609 16.891Q795.5 458.781 795.5 482.5v431q0 23.719-16.891 40.609Q761.719 971 738 971H222Zm0-57.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222 482.5v431-431Z"/></svg> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg new file mode 100644 index 0000000000..3188cf798e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M222-142.5h516v-431H222v431Zm258.084-140q31.179 0 53.297-21.566 22.119-21.566 22.119-51.85 0-29.347-22.203-53.465-22.203-24.119-53.381-24.119-31.179 0-53.297 24.035-22.119 24.034-22.119 53.881t22.203 51.465q22.203 21.619 53.381 21.619ZM222-142.5v-431 431Zm0 57.5q-23.719 0-40.609-16.891Q164.5-118.781 164.5-142.5v-431q0-23.719 16.891-40.609Q198.281-631 222-631h329.5v-95.018q0-77.832 54.349-132.157Q660.198-912.5 738-912.5q70 0 121.25 44T922-759q2 11.5-6.638 22.25T895.75-726q-12.66 0-20.705-6-8.045-6-9.545-18.5-9-44.5-44.55-74.5T738-855q-54.333 0-91.667 37.333Q609-780.333 609-726.231V-631h129q23.719 0 40.609 16.891Q795.5-597.219 795.5-573.5v431q0 23.719-16.891 40.609Q761.719-85 738-85H222Z"/></svg> \ No newline at end of file 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 4dc63182f0..846cd931a2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -212,7 +212,7 @@ \nPer connetterti, chiedi al tuo contatto di creare un altro link di connessione e controlla di avere una connessione di rete stabile.</string> <string name="error_smp_test_certificate">Probabilmente l\'impronta del certificato nell\'indirizzo del server è sbagliata</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Per rispettare la tua privacy, invece delle notifiche push l\'app ha un <b>servizio SimpleX in secondo piano</b>; usa una piccola percentuale di batteria al giorno.]]></string> - <string name="turn_off_battery_optimization"><![CDATA[Per poterlo usare, <b>disattiva l\'ottimizzazione della batteria</b> per SimpleX nella prossima schermata. Altrimenti le notifiche saranno disattivate.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Per usarlo, <b>consenti a SimpleX di funzionare in secondo piano</b> nella prossima schermata. Altrimenti le notifiche saranno disattivate.]]></string> <string name="simplex_service_notification_title">Servizio SimpleX Chat</string> <string name="notifications_mode_service_desc">Servizio in secondo piano sempre attivo. Le notifiche verranno mostrate appena i messaggi saranno disponibili.</string> <string name="la_notice_title_simplex_lock">SimpleX Lock</string> @@ -270,8 +270,6 @@ <string name="allow_your_contacts_to_send_voice_messages">Permetti ai tuoi contatti di inviare messaggi vocali.</string> <string name="chat_database_deleted">Database della chat eliminato</string> <string name="settings_section_title_icon">ICONA APP</string> - <string name="incognito_random_profile_from_contact_description">Verrà inviato un profilo casuale al contatto da cui hai ricevuto questo link</string> - <string name="incognito_random_profile_description">Verrà inviato un profilo casuale al tuo contatto</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione (NO servizio in secondo piano).]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Consuma più batteria</b>! Servizio in secondo piano sempre attivo: le notifiche sono mostrate non appena i messaggi sono disponibili.]]></string> <string name="callstatus_calling">chiamata…</string> @@ -422,7 +420,7 @@ <string name="rcv_group_event_changed_your_role">cambiato il tuo ruolo in %s</string> <string name="rcv_conn_event_switch_queue_phase_changing">cambio indirizzo…</string> <string name="snd_conn_event_switch_queue_phase_changing_for_member">cambio indirizzo per %s…</string> - <string name="rcv_group_event_member_connected">connesso</string> + <string name="rcv_group_event_member_connected">connesso/a</string> <string name="group_member_status_connected">connesso/a</string> <string name="group_member_status_accepted">in connessione (accettato)</string> <string name="group_member_status_announced">in connessione (annunciato)</string> @@ -568,7 +566,7 @@ <string name="icon_descr_address">Indirizzo di SimpleX</string> <string name="icon_descr_simplex_team">Squadra di SimpleX</string> <string name="you_accepted_connection">Hai accettato la connessione</string> - <string name="you_invited_your_contact">Hai invitato il contatto</string> + <string name="you_invited_a_contact">Hai invitato il contatto</string> <string name="your_chat_profile_will_be_sent_to_your_contact">Il tuo profilo di chat verrà inviato \nal tuo contatto</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Il tuo contatto deve essere in linea per completare la connessione. @@ -582,7 +580,6 @@ <string name="mark_code_verified">Segna come verificato/a</string> <string name="one_time_link">Link di invito una tantum</string> <string name="paste_button">Incolla</string> - <string name="paste_connection_link_below_to_connect">Incolla il link che hai ricevuto nella casella sottostante per connetterti con il tuo contatto.</string> <string name="smp_servers_preset_server">Server preimpostato</string> <string name="smp_servers_preset_address">Indirizzo server preimpostato</string> <string name="smp_servers_save">Salva i server</string> @@ -606,7 +603,6 @@ <string name="smp_servers_use_server_for_new_conn">Usa per connessioni nuove</string> <string name="smp_servers_use_server">Usa il server</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Puoi anche connetterti cliccando il link. Se si apre nel browser, clicca il pulsante <b>Apri nell\'app mobile</b>.]]></string> - <string name="your_profile_will_be_sent">Il tuo profilo di chat verrà inviato al tuo contatto</string> <string name="smp_servers_your_server">Il tuo server</string> <string name="smp_servers_your_server_address">L\'indirizzo del tuo server</string> <string name="your_simplex_contact_address">Il tuo indirizzo SimpleX</string> @@ -616,7 +612,8 @@ <string name="network_and_servers">Rete e server</string> <string name="network_settings_title">Impostazioni di rete</string> <string name="network_use_onion_hosts_no">No</string> - <string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione.</string> + <string name="network_use_onion_hosts_required_desc">Gli host Onion saranno necessari per la connessione. +\nNota bene: non potrai connetterti ai server senza indirizzo .onion .</string> <string name="network_use_onion_hosts_required_desc_in_alert">Gli host Onion saranno necessari per la connessione.</string> <string name="network_use_onion_hosts_prefer_desc">Gli host Onion verranno usati quando disponibili.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Gli host Onion verranno usati quando disponibili.</string> @@ -763,14 +760,14 @@ <string name="wrong_passphrase">Password del database sbagliata</string> <string name="wrong_passphrase_title">Password sbagliata!</string> <string name="you_are_invited_to_group_join_to_connect_with_group_members">Sei stato/a invitato/a al gruppo. Entra per connetterti con i suoi membri.</string> - <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puoi avviare la chat tramite Impostazioni -> Database o riavviando l\'app.</string> + <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">Puoi avviare la chat tramite Impostazioni / Database o riavviando l\'app.</string> <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante.</string> <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Non riceverai più messaggi da questo gruppo. La cronologia della chat verrà conservata.</string> <string name="group_member_status_invited">ha invitato</string> <string name="rcv_group_event_invited_via_your_group_link">invitato via link del tuo gruppo</string> <string name="rcv_group_event_member_added">ha invitato %1$s</string> - <string name="rcv_group_event_member_left">uscito/a</string> - <string name="group_member_status_left">è uscito/a</string> + <string name="rcv_group_event_member_left">è uscito/a</string> + <string name="group_member_status_left">uscito/a</string> <string name="group_member_role_member">membro</string> <string name="group_member_role_owner">proprietario</string> <string name="group_member_status_removed">ha rimosso</string> @@ -814,8 +811,7 @@ <string name="invite_prohibited_description">Stai tentando di invitare un contatto con cui hai condiviso un profilo in incognito nel gruppo in cui stai usando il tuo profilo principale</string> <string name="group_info_member_you">tu: %1$s</string> <string name="incognito">Incognito</string> - <string name="group_unsupported_incognito_main_profile_sent">La modalità in incognito non è supportata qui: il tuo profilo principale verrà inviato ai membri del gruppo</string> - <string name="incognito_info_protects">La modalità in incognito protegge la privacy del nome e dell\'immagine del tuo profilo principale: per ogni nuovo contatto viene creato un nuovo profilo casuale.</string> + <string name="incognito_info_protects">La modalità in incognito protegge la tua privacy usando un nuovo profilo casuale per ogni contatto.</string> <string name="conn_level_desc_indirect">indiretta (%1$s)</string> <string name="incognito_info_allows">Permette di avere molte connessioni anonime senza dati condivisi tra di loro in un unico profilo di chat.</string> <string name="theme_light">Chiaro</string> @@ -836,7 +832,6 @@ <string name="group_is_decentralized">Il gruppo è completamente decentralizzato: è visibile solo ai membri.</string> <string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string> <string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string> - <string name="incognito_info_find">Per trovare il profilo usato per una connessione in incognito, tocca il nome del contatto o del gruppo in cima alla chat.</string> <string name="update_network_settings_confirmation">Aggiorna</string> <string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string> <string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string> @@ -1325,7 +1320,7 @@ <string name="receipts_contacts_override_disabled">L\'invio di ricevute è disattivato per %d contatti</string> <string name="sync_connection_force_desc">La crittografia funziona e il nuovo accordo sulla crittografia non è richiesto. Potrebbero verificarsi errori di connessione!</string> <string name="receipts_section_description">Queste impostazioni sono per il tuo profilo attuale</string> - <string name="receipts_section_description_1">Possono essere sovrascritte nelle impostazioni dei contatti</string> + <string name="receipts_section_description_1">Possono essere sovrascritte nelle impostazioni dei contatti e dei gruppi.</string> <string name="receipts_contacts_enable_for_all">Attiva per tutti</string> <string name="receipts_contacts_enable_keep_overrides">Attiva (mantieni sostituzioni)</string> <string name="receipts_contacts_override_enabled">L\'invio di ricevute è attivo per %d contatti</string> @@ -1342,4 +1337,70 @@ <string name="in_reply_to">In risposta a</string> <string name="no_history">Nessuna cronologia</string> <string name="sync_connection_force_question">Rinegoziare la crittografia\?</string> + <string name="choose_file_title">Scegli un file</string> + <string name="no_selected_chat">Nessuna chat selezionata</string> + <string name="in_developing_title">Prossimamente!</string> + <string name="in_developing_desc">Questa funzione non è ancora supportata. Prova la prossima versione.</string> + <string name="no_info_on_delivery">Nessuna informazione sulla consegna</string> + <string name="receipts_groups_title_enable">Attivare le ricevute per i gruppi\?</string> + <string name="receipts_section_groups">Piccoli gruppi (max 20)</string> + <string name="receipts_groups_enable_for_all">Attiva per tutti i gruppi</string> + <string name="receipts_groups_override_disabled">L\'invio di ricevute è disattivato per %d gruppi</string> + <string name="receipts_groups_override_enabled">L\'invio di ricevute è attivato per %d gruppi</string> + <string name="send_receipts_disabled_alert_title">Le ricevute sono disattivate</string> + <string name="delivery">Consegna</string> + <string name="receipts_groups_enable_keep_overrides">Attiva (mantieni sostituzioni dei gruppi)</string> + <string name="send_receipts_disabled">disattivato</string> + <string name="receipts_groups_disable_keep_overrides">Disattiva (mantieni sostituzioni dei gruppi)</string> + <string name="receipts_groups_title_disable">Disattivare le ricevute per i gruppi\?</string> + <string name="receipts_groups_disable_for_all">Disattiva per tutti i gruppi</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled_alert_msg">Questo gruppo ha più di %1$d membri, le ricevute di consegna non vengono inviate.</string> + <string name="connect_via_member_address_alert_title">Connettersi direttamente\?</string> + <string name="connect_via_member_address_alert_desc">La richiesta di connessione verrà inviata a questo membro del gruppo.</string> + <string name="connect_via_link_incognito">Connetti in incognito</string> + <string name="connect_use_current_profile">Usa il profilo attuale</string> + <string name="connect_use_new_incognito_profile">Usa nuovo profilo in incognito</string> + <string name="turn_off_battery_optimization_button">Consenti</string> + <string name="turn_off_system_restriction_button">Apri impostazioni app</string> + <string name="system_restricted_background_in_call_desc">L\'app potrebbe venire chiusa dopo 1 minuto in secondo piano.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Incolla il link che hai ricevuto per connetterti con il contatto…</string> + <string name="connect__your_profile_will_be_shared">Il tuo profilo %1$s verrà condiviso.</string> + <string name="connect__a_new_random_profile_will_be_shared">Verrà condiviso un nuovo profilo casuale.</string> + <string name="disable_notifications_button">Disattiva le notifiche</string> + <string name="system_restricted_background_in_call_title">Nessuna chiamata in secondo piano</string> + <string name="system_restricted_background_desc">SimpleX non può funzionare in secondo piano. Riceverai le notifiche solo quando l\'app è aperta.</string> + <string name="system_restricted_background_warn"><![CDATA[Per attivare le notifiche, scegli <b>Utilizzo batteria dell\'app</b> / <b>Senza restrizioni</b> nelle impostazioni.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Per effettuare chiamate in secondo piano, scegli <b>Utilizzo batteria dell\'app</b> / <b>Senza restrizioni</b> nelle impostazioni.]]></string> + <string name="rcv_group_event_2_members_connected">%s e %s sono connessi/e</string> + <string name="rcv_group_event_n_members_connected">%s, %s e altri %d membri sono connessi</string> + <string name="rcv_group_event_3_members_connected">%s, %s e %s sono connessi/e</string> + <string name="privacy_message_draft">Bozza</string> + <string name="privacy_show_last_messages">Mostra gli ultimi messaggi</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Il database verrà crittografato e la password conservata nelle impostazioni.</string> + <string name="you_can_change_it_later">La password casuale viene conservata nelle impostazioni come testo normale. +\nPuoi cambiarla dopo.</string> + <string name="database_encryption_will_be_updated_in_settings">La password di crittografia del database verrà aggiornata e conservata nelle impostazioni.</string> + <string name="remove_passphrase_from_settings">Rimuovere la password dalle impostazioni\?</string> + <string name="use_random_passphrase">Usa password casuale</string> + <string name="save_passphrase_in_settings">Salva password nelle impostazioni</string> + <string name="setup_database_passphrase">Configura password del database</string> + <string name="set_database_passphrase">Imposta password del database</string> + <string name="open_database_folder">Apri cartella del database</string> + <string name="passphrase_will_be_saved_in_settings">La password verrà conservata nelle impostazioni come testo normale dopo averla cambiata o il riavvio dell\'app.</string> + <string name="settings_is_storing_in_clear_text">La password viene conservata nelle impostazioni come testo normale.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Nota bene</b>: i relay di messaggi e file sono connessi via proxy SOCKS. Le chiamate e l\'invio di anteprime dei link usano una connessione diretta.]]></string> + <string name="encrypt_local_files">Cripta i file locali</string> + <string name="v5_3_encrypt_local_files">Crittografia di file e media memorizzati</string> + <string name="v5_3_new_desktop_app">Nuova app desktop!</string> + <string name="v5_3_new_interface_languages">6 nuove lingue dell\'interfaccia</string> + <string name="v5_3_encrypt_local_files_descr">L\'app cripta i nuovi file locali (eccetto i video).</string> + <string name="v5_3_discover_join_groups">Scopri ed unisciti ai gruppi</string> + <string name="v5_3_simpler_incognito_mode">Modalità incognito semplificata</string> + <string name="v5_3_new_interface_languages_descr">Arabo, bulgaro, finlandese, ebraico, tailandese e ucraino - grazie agli utenti e a Weblate.</string> + <string name="v5_3_new_desktop_app_descr">Crea un nuovo profilo nell\'app desktop. 💻</string> + <string name="v5_3_simpler_incognito_mode_descr">Attiva/disattiva l\'incognito quando ti colleghi.</string> + <string name="v5_3_discover_join_groups_descr">- connessione al servizio directory (BETA)! +\n- ricevute di consegna (fino a 20 membri). +\n- più veloce e più stabile.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index e165588a6b..8467199bf4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -59,8 +59,6 @@ <string name="notifications_mode_off_desc">האפליקציה יכולה לקבל התראות רק כאשר היא מופעלת, לא יופעל שירות ברקע.</string> <string name="v5_0_app_passcode">קוד גישה לאפליקציה</string> <string name="app_version_title">גרסת האפליקציה</string> - <string name="incognito_random_profile_description">פרופיל אקראי יישלח לאיש הקשר</string> - <string name="incognito_random_profile_from_contact_description">פרופיל אקראי יישלח לאיש הקשר שממנו קיבלת קישור זה</string> <string name="network_session_mode_user_description"><![CDATA[חיבור TCP נפרד (ואישור SOCKS) ייווצר <b>לכל פרופיל צ׳אט שיש ברשותך באפליקציה</b>.]]></string> <string name="network_session_mode_entity_description">חיבור TCP נפרד (ואישור SOCKS) ייווצר <b>לכל איש קשר וחבר קבוצה</b>. \n<b>שימו לב</b>: אם ברשותכם חיבורים רבים, צריכת הסוללה ותעבורת האינטרנט עשויה להיות גבוהה משמעותית וחלק מהחיבורים עלולים להיכשל.</string> @@ -526,7 +524,6 @@ <string name="import_database_confirmation">ייבא</string> <string name="import_theme">ייבא ערכת נושא</string> <string name="import_theme_error">שגיאה בייבוא ערכת נושא</string> - <string name="group_unsupported_incognito_main_profile_sent">מצב זהות נסתרת אינו נתמך כאן – הפרופיל הראשי שלך יישלח לחברי הקבוצה</string> <string name="v4_3_improved_server_configuration">תצורת שרתים משופרת</string> <string name="v4_3_improved_privacy_and_security">פרטיות ואבטחה משופרים</string> <string name="settings_section_title_incognito">מצב זהות נסתרת</string> @@ -535,12 +532,12 @@ <string name="description_via_group_link_incognito">זהות נסתרת באמצעות קישור קבוצה</string> <string name="description_via_one_time_link_incognito">זהות נסתרת באמצעות קישור חד־פעמי</string> <string name="invalid_connection_link">קישור חיבור לא תקין</string> - <string name="turn_off_battery_optimization"><![CDATA[על מנת להשתמש בזה, אנא <b>השביתו מיטוב סוללה</b> עבור SimpleX בתיבת הדו־שיח הבאה. אחרת, ההתראות יושבתו.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[בשביל להשתמש בזה, אנא <b>אפשרו ל-SimpleX לפעול ברקע</b> בתיבת הדו-שיח הבאה. אחרת, ההתראות יושבתו.]]></string> <string name="service_notifications_disabled">התראות מיידיות מושבתות!</string> <string name="icon_descr_add_members">הזמן חברי קבוצה</string> <string name="group_member_status_invited">הוזמן</string> <string name="conn_level_desc_indirect">עקיף (%1$s)</string> - <string name="incognito_info_protects">מצב זהות נסתרת מגן על התמונה והפרטיות של שם הפרופיל הראשי שלך – עבור כל איש קשר חדש נוצר פרופיל חדש אקראי.</string> + <string name="incognito_info_protects">מצב זהות נסתרת מגן על הפרטיות שלך על ידי שימוש בפרופיל אקראי חדש עבור כל איש קשר.</string> <string name="incompatible_database_version">גרסת מסד נתונים לא תואמת</string> <string name="invalid_migration_confirmation">אישור העברת נתונים לא תקין</string> <string name="v4_3_irreversible_message_deletion">מחיקה בלתי הפיכה של הודעות</string> @@ -692,7 +689,7 @@ <string name="old_database_archive">ארכיון מסד נתונים ישן</string> <string name="chat_item_ttl_none">לעולם לא</string> <string name="no_received_app_files">לא התקבלו או נשלחו קבצים</string> - <string name="new_member_role">תפקיד חבר קבוצה</string> + <string name="new_member_role">תפקיד חבר קבוצה חדש</string> <string name="no_contacts_selected">לא נבחרו אנשי קשר</string> <string name="member_info_section_title_member">חבר קבוצה</string> <string name="member_will_be_removed_from_group_cannot_be_undone">חבר הקבוצה יוסר מהקבוצה – לא ניתן לבטל זאת!</string> @@ -766,7 +763,6 @@ <string name="auth_open_chat_console">פתיחת מסוף צ׳אט</string> <string name="auth_open_chat_profiles">פתיחת פרופילי צ׳אט</string> <string name="icon_descr_server_status_pending">ממתין</string> - <string name="paste_connection_link_below_to_connect">הדביקו את הקישור שקיבלתם בתיבה למטה כדי להתחבר לאיש הקשר שלכם.</string> <string name="smp_servers_preset_address">כתובת שרת מוגדר מראש</string> <string name="password_to_show">סיסמה להצגה</string> <string name="onboarding_notifications_mode_title">התראות פרטיות</string> @@ -1048,7 +1044,6 @@ <string name="alert_text_msg_bad_hash">הגיבוב של ההודעה הקודמת שונה.</string> <string name="enable_automatic_deletion_message">לא ניתן לבטל פעולה זו - ההודעות שנשלחו והתקבלו לפני הזמן שנבחר יימחקו. זה עשוי להימשך מספר דקות.</string> <string name="alert_message_no_group">הקבוצה הזו כבר לא קיימת.</string> - <string name="incognito_info_find">כדי למצוא את הפרופיל המשמש לזהות נסתרת, הקישו על שם איש הקשר או הקבוצה בחלק העליון של הצ\'אט.</string> <string name="image_decoding_exception_desc">לא ניתן לפענח את התמונה. אנא נסו תמונה אחרת או צרו קשר עם המפתחים.</string> <string name="videos_limit_title">יותר מדי סרטונים!</string> <string name="switch_receiving_address_desc">כתובת הקבלה תשתנה לשרת אחר. שינוי הכתובת יושלם לאחר שהשולח יתחבר לאינטרנט.</string> @@ -1182,7 +1177,7 @@ <string name="waiting_for_video">ממתין לסרטון</string> <string name="waiting_for_file">ממתין לקובץ</string> <string name="voice_messages_prohibited">הודעות קוליות אסורות!</string> - <string name="you_invited_your_contact">הזמנת את איש הקשר שלך</string> + <string name="you_invited_a_contact">הזמנת את איש הקשר שלך</string> <string name="you_can_share_your_address">באפשרותכם לשתף את הכתובת שלכם כקישור או כקוד QR – כל אחד יכול להתחבר אליכם.</string> <string name="you_can_accept_or_reject_connection">כאשר אנשים מבקשים להתחבר, באפשרותך לקבל או לדחות זאת.</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[באפשרותכם גם להתחבר על־ידי לחיצה על הקישור. אם הוא נפתח בדפדפן, ליחצו על הכפתור <b>פתח באפליקציה</b>.]]></string> @@ -1222,7 +1217,6 @@ \nניתן לבטל חיבור זה ולהסיר את איש הקשר (ולנסות מאוחר יותר עם קישור חדש).</string> <string name="your_chat_profile_will_be_sent_to_your_contact">פרופיל הצ׳אט שלך יישלח \nלאיש הקשר שלך</string> - <string name="your_profile_will_be_sent">פרופיל הצ׳אט שלך יישלח לאיש הקשר שלך</string> <string name="your_chat_database">מסד הנתונים שלך</string> <string name="you_rejected_group_invitation">דחית את ההזמנה לקבוצה</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">מסד הנתונים הנוכחי שלך יימחק ויוחלף במסד הנתונים המיובא. @@ -1263,7 +1257,7 @@ <string name="your_preferences">ההעדפות שלך</string> <string name="you_will_still_receive_calls_and_ntfs">עדיין תקבלו שיחות והתראות מפרופילים מושתקים כאשר הם פעילים.</string> <string name="abort_switch_receiving_address_confirm">בטל</string> - <string name="abort_switch_receiving_address_question">בטל שינוי כתובת\?</string> + <string name="abort_switch_receiving_address_question">האם לבטל שינוי כתובת\?</string> <string name="abort_switch_receiving_address_desc">שינוי הכתובת יבוטל. ייעשה שימוש בכתובת הקבלה הישנה.</string> <string name="shutdown_alert_desc">ההתראות יפסיקו לפעול עד שתפעיל את האפליקציה מחדש</string> <string name="abort_switch_receiving_address">בטל שינוי כתובת</string> @@ -1331,7 +1325,7 @@ \n- ועוד!</string> <string name="sending_delivery_receipts_will_be_enabled_all_profiles">שליחת קבלות שליחה תתאפשר עבור כל אנשי הקשר בכל פרופילי הצ\'אט הגלויים.</string> <string name="receipts_section_description">הגדרות אלו מיועדות לפרופיל הנוכחי שלך</string> - <string name="receipts_section_description_1">ניתן לעקוף אותם בהגדרות אנשי הקשר</string> + <string name="receipts_section_description_1">ניתן לעקוף אותם בהגדרות אנשי קשר וקבוצות.</string> <string name="receipts_contacts_override_disabled">שליחת קבלות מושבתת עבור %d אנשי קשר</string> <string name="receipts_contacts_override_enabled">שליחת קבלות מאופשרת עבור %d אנשי קשר</string> <string name="settings_section_title_delivery_receipts">שלח קבלות משלוח אל</string> @@ -1340,4 +1334,47 @@ <string name="sending_delivery_receipts_will_be_enabled">שליחת קבלות שליחה תתאפשר עבור כל אנשי הקשר.</string> <string name="you_can_enable_delivery_receipts_later">תוכל לאפשר מאוחר יותר דרך הגדרות</string> <string name="you_can_enable_delivery_receipts_later_alert">תוכל לאפשר אותם מאוחר יותר באמצעות הגדרות פרטיות ואבטחה של האפליקציה.</string> + <string name="v5_2_message_delivery_receipts">קבלות על הודעות!</string> + <string name="v5_2_disappear_one_message_descr">גם אם הוא מושבת בשיחה.</string> + <string name="v5_2_fix_encryption">שימרו על הקשרים שלכם</string> + <string name="choose_file_title">בחר קובץ</string> + <string name="connect_via_link_incognito">התחבר בזהות נסתרת</string> + <string name="connect_use_current_profile">השתמש בפרופיל הנוכחי</string> + <string name="connect_use_new_incognito_profile">השתמש בפרופיל זהות נסתרת חדש</string> + <string name="disable_notifications_button">השבת התראות</string> + <string name="turn_off_system_restriction_button">פתח את הגדרות האפליקציה</string> + <string name="system_restricted_background_in_call_title">ללא שיחות ברקע</string> + <string name="connect__a_new_random_profile_will_be_shared">ישותף פרופיל אקראי חדש.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">הדבק את הקישור שקיבלת כדי להתחבר לאיש הקשר שלך…</string> + <string name="connect__your_profile_will_be_shared">הפרופיל שלך %1$s ישותף.</string> + <string name="send_receipts_disabled_alert_title">קבלות מושבתות</string> + <string name="recipient_colon_delivery_status">%s :%s</string> + <string name="no_selected_chat">לא נבחר צ\'אט</string> + <string name="in_developing_title">בקרוב!</string> + <string name="delivery">מסירה</string> + <string name="no_info_on_delivery">אין מידע על מסירה</string> + <string name="privacy_message_draft">טיוטת הודעה</string> + <string name="receipts_groups_override_disabled">שליחת קבלות מושבתת עבור %d קבוצות</string> + <string name="receipts_groups_override_enabled">שליחת קבלות מופעלת עבור %d קבוצות</string> + <string name="privacy_show_last_messages">הצג הודעות אחרונות</string> + <string name="send_receipts_disabled">מושבת</string> + <string name="turn_off_battery_optimization_button">אפשר</string> + <string name="receipts_groups_disable_for_all">השבת לכל הקבוצות</string> + <string name="receipts_groups_disable_keep_overrides">השבת (שמור על עקיפות קבוצה)</string> + <string name="receipts_groups_title_disable">להשבית קבלות לקבוצות\?</string> + <string name="receipts_groups_enable_for_all">הפעל עבור כל הקבוצות</string> + <string name="receipts_groups_enable_keep_overrides">הפעל (שמור על עקיפות קבוצה)</string> + <string name="receipts_groups_title_enable">להפעיל קבלות לקבוצות\?</string> + <string name="rcv_group_event_2_members_connected">%s ו-%s מחוברים</string> + <string name="system_restricted_background_desc">SimpleX לא יכול לרוץ ברקע. תקבל את ההתראות רק כשהאפליקציה פועלת.</string> + <string name="rcv_group_event_n_members_connected">%s, %s ו-%d חברים אחרים מחוברים</string> + <string name="system_restricted_background_in_call_desc">האפליקציה עשויה להיסגר לאחר דקה אחת ברקע.</string> + <string name="send_receipts_disabled_alert_msg">לקבוצה זו יש יותר מ-%1$d חברים, קבלות מסירה לא נשלחות.</string> + <string name="rcv_group_event_3_members_connected">%s, %s ו-%s מחוברים</string> + <string name="in_developing_desc">מאפיין זה אינו נתמך עדיין. נסה את המהדורה הבאה.</string> + <string name="receipts_section_groups">קבוצות קטנות (מקסימום 20)</string> + <string name="system_restricted_background_warn"><![CDATA[כדי להפעיל התראות, בחר <b>שימוש בסוללה באפליקציה</b> / <b>ללא הגבלה</b> בהגדרות האפליקציה.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[כדי לבצע שיחות ברקע, בחר <b>שימוש בסוללה באפליקציה</b> / <b>ללא הגבלה</b> בהגדרות האפליקציה.]]></string> + <string name="connect_via_member_address_alert_title">להתחבר ישירות\?</string> + <string name="connect_via_member_address_alert_desc">בקשת חיבור תישלח לחבר קבוצה זה.</string> </resources> \ No newline at end of file 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 c3f5dbfec2..8b955ceb8f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -68,8 +68,6 @@ <string name="answer_call">通話に応答</string> <string name="settings_section_title_icon">アプリのアイコン</string> <string name="full_backup">アプリデータのバックアップ</string> - <string name="incognito_random_profile_from_contact_description">このリンクの送信元にランダムなプロフィール(ダミー)が送られます。</string> - <string name="incognito_random_profile_description">連絡先にランダムなプロフィール(ダミー)が送られます。</string> <string name="audio_call_no_encryption">音声通話 (エンドツーエンド暗号化なし)</string> <string name="icon_descr_asked_to_receive">画像受信を依頼しました。</string> <string name="auth_unavailable">認証不可能</string> @@ -169,7 +167,6 @@ <string name="icon_descr_call_ended">通話が終了しました。</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[直接会えない時は、 <b>ビデオ通話中にQRコードを見せてもらうか</b>、招待リンクを送ってもらえば相手に繋がります。]]></string> <string name="member_will_be_removed_from_group_cannot_be_undone">メンバーをグループから除名する (※元に戻せません※)!</string> - <string name="message_delivery_error_title">メッセージ送信エラー</string> <string name="call_on_lock_screen">通話をロック画面に表示</string> <string name="icon_descr_cancel_link_preview">リンクのプレビューを中止</string> <string name="icon_descr_cancel_live_message">ライブメッセージを中止</string> @@ -196,7 +193,6 @@ <string name="v4_3_improved_server_configuration">サーバ設定の向上</string> <string name="v4_3_improved_privacy_and_security">プライバシーとセキュリティ強化</string> <string name="settings_section_title_incognito">シークレットモード</string> - <string name="group_unsupported_incognito_main_profile_sent">ここではシークレットモードが無効です。メインのプロフィールがグループのメンバーに送られます。</string> <string name="new_in_version">%s バージョンアップで新しい</string> <string name="new_passphrase">新しい暗証フレーズ</string> <string name="chat_item_ttl_none">一度も</string> @@ -313,7 +309,6 @@ <string name="invalid_contact_link">無効なリンク!</string> <string name="invalid_QR_code">無効なQRコード</string> <string name="icon_descr_more_button">つづき</string> - <string name="paste_connection_link_below_to_connect">連絡相手から頂いたリンクを以下の入力欄に貼り付けて繋がります。</string> <string name="incorrect_code">誤ったセキュリティコード!</string> <string name="paste_button">貼り付け</string> <string name="chat_console">チャットのコンソール</string> @@ -489,7 +484,7 @@ <string name="smp_server_test_create_queue">サーバの待ち行列を作成する</string> <string name="smp_server_test_delete_queue">待ち行列を削除</string> <string name="smp_server_test_disconnect">切断</string> - <string name="turn_off_battery_optimization"><![CDATA[利用するには次の画面にてSimpleXに対する <b>電気省電力の設定をオフ</b> for SimpleX してください。そうしないと通知が無効になります。]]></string> + <string name="turn_off_battery_optimization"><![CDATA[利用するには次の画面にてSimpleXに対する <b>SimpleX のバックグラウンドでの実行を許可</b> してください。そうしないと通知が無効になります。]]></string> <string name="database_initialization_error_title">データベースを起動できません。</string> <string name="settings_notification_preview_title">通知のプレビュー</string> <string name="simplex_service_notification_text">メッセージ受信中…</string> @@ -535,7 +530,8 @@ <string name="how_to_use_simplex_chat">使い方</string> <string name="markdown_help">マークダウン (書式編集) ガイド</string> <string name="smp_servers_enter_manually">サーバを手動で入力</string> - <string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。</string> + <string name="network_use_onion_hosts_required_desc">接続にオニオンのホストが必要となります。 +\n注意: .onion アドレスがないとサーバーに接続できません。</string> <string name="create_address">アドレスを作成</string> <string name="delete_address__question">アドレスを削除しますか?</string> <string name="display_name__field">表示の名前:</string> @@ -592,7 +588,7 @@ <string name="error_removing_member">メンバー除名にエラー発生</string> <string name="conn_level_desc_indirect">関節 (%1$s)</string> <string name="incognito">シークレットモード</string> - <string name="incognito_info_protects">シークレットモードとは、メインのプロフィールとプロフィール画像を守るために、新しい連絡先を追加する時に、その連絡先に対してランダムなプロフィールが作成されるという対策です。</string> + <string name="incognito_info_protects">シークレット モードでは、連絡先ごとに新しいランダムなプロファイルを使用してプライバシーを保護します。</string> <string name="chat_preferences_no">いいえ</string> <string name="chat_preferences_on">オン</string> <string name="direct_messages">ダイレクトメッセージ</string> @@ -715,7 +711,7 @@ <string name="thank_you_for_installing_simplex">SimpleX Chatをご利用いただきありがとうございます!</string> <string name="use_camera_button">カメラ</string> <string name="you_accepted_connection">繋がりを承認しました</string> - <string name="you_invited_your_contact">連絡先に招待を送りました</string> + <string name="you_invited_a_contact">連絡先に招待を送りました</string> <string name="connection_you_accepted_will_be_cancelled">承認ずみの接続がキャンセルされます!</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">あなたからリンクを受けた連絡先が接続できなくなります!</string> <string name="icon_descr_address">SimpleXアドレス</string> @@ -727,7 +723,6 @@ <string name="your_chat_profile_will_be_sent_to_your_contact">あなたのチャットプロフィールが \n連絡相手に送られます。</string> <string name="share_invitation_link">ワンタイムリンクを送る</string> - <string name="your_profile_will_be_sent">あなたのチャットプロフィールが連絡相手に送られます。</string> <string name="scan_code">コードを読み込む</string> <string name="scan_code_from_contacts_app">連絡相手のアプリからセキュリティコードを読み込む</string> <string name="chat_lock">SimpleXロック</string> @@ -909,7 +904,7 @@ <string name="simplex_link_mode">SimpleXリンク</string> <string name="settings_section_title_support">SIMPLEX CHATを支援</string> <string name="smp_servers_test_servers">テストサーバ</string> - <string name="switch_receiving_address_desc">開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。</string> + <string name="switch_receiving_address_desc">受信アドレスは別のサーバーに変更されます。アドレス変更は送信者がオンラインになった後に完了します。</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[あなたのプライバシーを守るために、このアプリはプッシュ通知の変わりに <b>SimpleX バックグラウンド・サービス</b> を使ってます。一日の電池使用量は約3%です。]]></string> <string name="to_protect_privacy_simplex_has_ids_for_queues">あなたのプライバシーを守るために、他のアプリと違って、ユーザーIDの変わりに SimpleX メッセージ束毎にIDを配布し、各連絡先が別々と扱います。</string> <string name="group_main_profile_sent">あなたのチャットプロフィールが他のグループメンバーに送られます。</string> @@ -933,7 +928,6 @@ <string name="you_will_be_connected_when_your_connection_request_is_accepted">連絡先が繋がりリクエストを承認したら、接続されます。後でチェックするか、しばらくお待ちください。</string> <string name="switch_receiving_address">受信アドレスを変える</string> <string name="incognito_random_profile">あなたのランダム・プロフィール</string> - <string name="incognito_info_find">シークレットモード接続のプロフィールを確認するには、チャットの上部の連絡先、またはグループ名をタップします。</string> <string name="v4_3_voice_messages">音声メッセージ</string> <string name="settings_section_title_socks">SOCKSプロキシ</string> <string name="stop_chat_to_export_import_or_delete_chat_database">データベースのエキスポート、読み込み、削除するにはチャットを閉じてからです。チャットを閉じると送受信ができなくなります。</string> @@ -1261,4 +1255,140 @@ <string name="v5_1_message_reactions_descr">ついに、私たちはそれらを手に入れました! 🚀</string> <string name="v5_1_better_messages">より良いメッセージ</string> <string name="la_mode_off">オフ</string> + <string name="choose_file_title">ファイルを選択</string> + <string name="unfavorite_chat">お気に入りを解除</string> + <string name="favorite_chat">お気に入り</string> + <string name="receipts_contacts_override_enabled">Sending receipts is enabled for %d contacts</string> + <string name="receipts_contacts_enable_for_all">すべて有効</string> + <string name="receipts_contacts_override_disabled">Sending receipts is disabled for %d contacts</string> + <string name="receipts_contacts_disable_for_all">すべて無効</string> + <string name="settings_shutdown">終了</string> + <string name="conn_event_ratchet_sync_started">暗号化に同意しています…</string> + <string name="conn_event_ratchet_sync_ok">暗号化OK</string> + <string name="conn_event_ratchet_sync_allowed">暗号化の再ネゴシエーションを許可</string> + <string name="conn_event_ratchet_sync_required">暗号化の再ネゴシエーションが必要</string> + <string name="snd_conn_event_ratchet_sync_agreed">%s の暗号化に同意しました</string> + <string name="snd_conn_event_ratchet_sync_ok">%s の暗号化 OK</string> + <string name="snd_conn_event_ratchet_sync_allowed">%s の暗号化の再ネゴシエーションを許可しました</string> + <string name="snd_conn_event_ratchet_sync_required">%s の暗号化の再ネゴシエーションが必要</string> + <string name="rcv_conn_event_verification_code_reset">セキュリティコードが変更されました</string> + <string name="fix_connection_confirm">修正</string> + <string name="fix_connection_question">接続を修正しますか\?</string> + <string name="renegotiate_encryption">暗号化の再ネゴシエート</string> + <string name="allow_to_send_files">ファイルやメディアの送信を許可</string> + <string name="prohibit_sending_files">ファイルやメディアの送信を禁止します。</string> + <string name="v5_2_fix_encryption">接続を維持</string> + <string name="v5_2_message_delivery_receipts_descr">長らくお待たせしました! ✅</string> + <string name="v5_2_more_things">その他</string> + <string name="enable_receipts_all">有効にする</string> + <string name="v5_2_more_things_descr">- より安定したメッセージ配信。 +\n- 改良されたグループ。 +\n- などなど!</string> + <string name="you_can_enable_delivery_receipts_later">あとで設定から有効にできます。</string> + <string name="dont_enable_receipts">有効にしない</string> + <string name="error_aborting_address_change">アドレス変更中止エラー</string> + <string name="no_filtered_chats">フィルタされたチャットはありません</string> + <string name="files_and_media_prohibited">ファイルとメディアは禁止されています!</string> + <string name="abort_switch_receiving_address_desc">アドレス変更は中止されます。古い受信アドレスが使用されます。</string> + <string name="abort_switch_receiving_address_question">アドレス変更を中止しますか?</string> + <string name="settings_section_title_app">アプリ</string> + <string name="non_fatal_errors_occured_during_import">インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。</string> + <string name="network_option_protocol_timeout_per_kb">KB あたりのプロトコル タイムアウト</string> + <string name="group_members_can_send_files">グループメンバーはファイルやメディアを送信できます。</string> + <string name="abort_switch_receiving_address">アドレス変更の中止</string> + <string name="files_are_prohibited_in_group">このグループでは、ファイルとメディアは禁止されています。</string> + <string name="shutdown_alert_question">終了しますか?</string> + <string name="shutdown_alert_desc">アプリを再起動するまで通知は機能しません。</string> + <string name="v5_2_favourites_filter_descr">未読とお気に入りをフィルターします。</string> + <string name="v5_2_favourites_filter">チャットを素早く検索</string> + <string name="v5_2_fix_encryption_descr">バックアップの復元後に暗号化を修正します。</string> + <string name="snd_conn_event_ratchet_sync_started">暗号化に同意しています: %s</string> + <string name="conn_event_ratchet_sync_agreed">暗号化に同意しました</string> + <string name="receipts_contacts_title_disable">Disable receipts\?</string> + <string name="fix_connection_not_supported_by_contact">連絡先による修正はサポートされていません</string> + <string name="fix_connection_not_supported_by_group_member">グループメンバーによる修正はサポートされていません</string> + <string name="receipts_section_contacts">連絡先</string> + <string name="receipts_section_description_1">これらは連絡先とグループの設定が優先されます。</string> + <string name="receipts_section_description">これらの設定は現在のプロファイル用です</string> + <string name="receipts_contacts_title_enable">Enable receipts\?</string> + <string name="sender_at_ts">%s : %s</string> + <string name="fix_connection">接続を修正</string> + <string name="files_and_media">ファイルとメディア</string> + <string name="you_can_enable_delivery_receipts_later_alert">あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。</string> + <string name="item_info_no_text">テキストなし</string> + <string name="error_synchronizing_connection">接続の同期エラー</string> + <string name="no_history">履歴はありません</string> + <string name="abort_switch_receiving_address_confirm">中止</string> + <string name="sync_connection_force_desc">暗号化は機能しており、新しい暗号化への同意は必要ありません。接続エラーが発生する可能性があります!</string> + <string name="sync_connection_force_question">暗号化を再ネゴシエートしますか\?</string> + <string name="in_reply_to">返信先</string> + <string name="only_owners_can_enable_files_and_media">ファイルやメディアを有効にできるのは、グループオーナーだけです。</string> + <string name="sync_connection_force_confirm">再ネゴシエート</string> + <string name="settings_restart_app">再起動</string> + <string name="connect_via_link_incognito">シークレットモードで接続</string> + <string name="turn_off_battery_optimization_button">許可</string> + <string name="connect_via_member_address_alert_title">直接接続しますか\?</string> + <string name="connect__a_new_random_profile_will_be_shared">新しいランダムなプロファイルが共有されます。</string> + <string name="delivery">送信</string> + <string name="in_developing_title">近日公開!</string> + <string name="v5_2_disappear_one_message">メッセージを1つ消す</string> + <string name="connect_use_current_profile">現在のプロファイルを使用する</string> + <string name="connect_use_new_incognito_profile">新しいシークレットプロファイルを使用する</string> + <string name="turn_off_system_restriction_button">アプリの設定を開く</string> + <string name="disable_notifications_button">通知を無効にする</string> + <string name="system_restricted_background_warn"><![CDATA[通知を有効にするには、アプリの設定で<b>アプリのバッテリー使用量</b> / <b>制限なし</b> を選択してください。]]></string> + <string name="system_restricted_background_in_call_desc">アプリはバックグラウンドで1分経過すると終了します。</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[バックグラウンドで通話を行うには、アプリの設定で<b>アプリのバッテリー使用量</b> / <b>制限なし</b> を選択してください。]]></string> + <string name="connect__your_profile_will_be_shared">あなたのプロフィール %1$s が共有されます。</string> + <string name="receipts_groups_title_enable">Enable receipts for groups\?</string> + <string name="receipts_groups_title_disable">Disable receipts for groups\?</string> + <string name="receipts_groups_override_enabled">Sending receipts is enabled for %d groups</string> + <string name="receipts_groups_override_disabled">Sending receipts is disabled for %d groups</string> + <string name="send_receipts_disabled">無効</string> + <string name="system_restricted_background_in_call_title">バックグラウンド通話なし</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">受信したリンクを貼り付け、連絡先に接続する。</string> + <string name="rcv_group_event_2_members_connected">%s と %s は接続中</string> + <string name="system_restricted_background_desc">SimpleXはバックグラウンドでは動作できません。アプリが起動している時のみ通知を受け取ることができます。</string> + <string name="no_info_on_delivery">送信情報なし</string> + <string name="no_selected_chat">チャットが選択されていません</string> + <string name="receipts_section_groups">小グループ(最大20名)</string> + <string name="receipts_groups_enable_for_all">すべてのグループで有効にする</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="connect_via_member_address_alert_desc">接続リクエストはこのグループ メンバーに送信されます。</string> + <string name="receipts_groups_disable_for_all">すべてのグループで無効にする</string> + <string name="privacy_message_draft">メッセージの下書き</string> + <string name="privacy_show_last_messages">最新のメッセージを表示</string> + <string name="send_receipts">Send receipts</string> + <string name="rcv_group_event_n_members_connected">%s, %s および %d 人の他のメンバーが接続しています。</string> + <string name="rcv_group_event_3_members_connected">%s, %s と %s は接続中</string> + <string name="in_developing_desc">この機能はまだサポートされていません。次のリリースをお試しください。</string> + <string name="receipts_contacts_enable_keep_overrides">有効にする(設定の優先を維持)</string> + <string name="receipts_groups_disable_keep_overrides">無効にする(グループの設定の優先を維持)</string> + <string name="receipts_groups_enable_keep_overrides">有効にする(グループの設定の優先を維持)</string> + <string name="receipts_contacts_disable_keep_overrides">無効にする(設定の優先を維持)</string> + <string name="v5_2_disappear_one_message_descr">会話中に無効になっている場合でも。</string> + <string name="v5_2_message_delivery_receipts">Message delivery receipts!</string> + <string name="delivery_receipts_title">Delivery receipts!</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Sending delivery receipts will be enabled for all contacts in all visible chat profiles.</string> + <string name="message_delivery_error_title">Message delivery error</string> + <string name="send_receipts_disabled_alert_title">Receipts are disabled</string> + <string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string> + <string name="sending_delivery_receipts_will_be_enabled">Sending delivery receipts will be enabled for all contacts.</string> + <string name="send_receipts_disabled_alert_msg">This group has over %1$d members, delivery receipts are not sent.</string> + <string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string> + <string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">データベースは暗号化され、パスフレーズは設定に保存されます。</string> + <string name="you_can_change_it_later">ランダムなパスフレーズは設定に平文として保存されます。 +\n後で変更できます。</string> + <string name="database_encryption_will_be_updated_in_settings">データベースの暗号化パスフレーズが更新され、設定に保存されます。</string> + <string name="remove_passphrase_from_settings">設定からパスフレーズを削除しますか?</string> + <string name="use_random_passphrase">ランダムなパスフレーズを使用する</string> + <string name="save_passphrase_in_settings">パスフレーズを設定に保存します</string> + <string name="setup_database_passphrase">データベースのパスフレーズを設定する</string> + <string name="set_database_passphrase">データベースのパスフレーズを設定する</string> + <string name="open_database_folder">データベースフォルダを開く</string> + <string name="passphrase_will_be_saved_in_settings">パスフレーズを変更するかアプリを再起動すると、平文として設定に保存されます。</string> + <string name="settings_is_storing_in_clear_text">パスフレーズは平文として設定に保存されます。</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>注意</b>: メッセージとファイルのリレーは SOCKS プロキシ経由で接続されます。 通話とリンク プレビューの送信には直接接続が使用されます。]]></string> + <string name="encrypt_local_files">ローカルファイルを暗号化する</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index 03fd372a25..bee70ddce6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -112,7 +112,6 @@ <string name="change_member_role_question">그룹 역할을 바꾸시겠습니까\?</string> <string name="info_row_connection">연결</string> <string name="users_add">프로필 추가</string> - <string name="incognito_random_profile_description">대화 상대에게 랜덤 프로필이 전송됩니다.</string> <string name="cant_delete_user_profile">사용자 프로필을 삭제할 수 없습니다!</string> <string name="chat_preferences_always">항상</string> <string name="chat_preferences_contact_allows">대화 상대가 허용함</string> @@ -161,7 +160,6 @@ <string name="notifications_mode_off_desc">앱이 실행 중일 때만 알림을 받을 수 있으며, 백그라운드 서비스는 시작되지 않습니다.</string> <string name="full_backup">앱 데이터 백업</string> <string name="settings_section_title_icon">앱 아이콘</string> - <string name="incognito_random_profile_from_contact_description">이 링크를 받은 사람한테 랜덤으로 만들어진 익명 프로필이 전송됩니다.</string> <string name="network_session_mode_user_description"><![CDATA[별도로 분리된 TCP 연결(그리고 SOCKS 자격 증명)이 <b>각각의 채팅 프로필</b>에 사용될 겁니다.]]></string> <string name="network_session_mode_entity_description">별도로 분리된 TCP 연결(및 SOCKS 자격 증명)이 <b>각각의 대화 상대 및 그룹 구성원</b>에게 사용될 겁니다. \n<b>참고</b>: 연결이 많은 경우 배터리 및 트래픽 소비가 높을 수 있고 일부 연결이 실패할 수 있습니다.</string> @@ -497,7 +495,6 @@ <string name="group_display_name_field">보여지는 그룹 이름</string> <string name="group_full_name_field">그룹 이름 :</string> <string name="group_is_decentralized">그룹은 완전히 탈중앙화되어 있으며 구성원만 그룹을 볼 수 있어요.</string> - <string name="group_unsupported_incognito_main_profile_sent">여기에서는 시크릿 모드가 지원되지 않아요. 기본 프로필이 그룹 멤버들에게 전송될 거예요.</string> <string name="group_main_profile_sent">프로필이 그룹 구성원에게 전송될 거예요.</string> <string name="group_profile_is_stored_on_members_devices">그룹 프로필은 서버가 아닌 멤버들의 기기에 저장되어요.</string> <string name="group_preferences">그룹 설정</string> @@ -561,7 +558,6 @@ <string name="import_database">데이터베이스 가져오기</string> <string name="import_database_confirmation">가져오기</string> <string name="incognito">익명 모드</string> - <string name="incognito_info_find">익명 채팅에 사용되는 프로필을 확인하려면 채팅 상단에 있는 연락처 또는 그룹 이름을 탭하세요.</string> <string name="image_will_be_received_when_contact_completes_uploading">대화 상대가 업로드를 완료하면 이미지가 수신될 거예요.</string> <string name="image_descr_profile_image">프로필 이미지</string> <string name="incognito_info_allows">하나의 프로필로 여러 사람과 연락할 필요 없이 무수히 많은 익명 프로필로 연락할 수 있어요.</string> @@ -746,7 +742,6 @@ <string name="protect_app_screen">앱 잠금</string> <string name="personal_welcome">%1$s님, 환영합니다!</string> <string name="only_stored_on_members_devices">(그룹 구성원에게만 저장됨)</string> - <string name="paste_connection_link_below_to_connect">아래 칸에 받은 링크를 붙여넣기하여 대화 상대와 연결해 주세요.</string> <string name="rate_the_app">앱 평가하기</string> <string name="onboarding_notifications_mode_periodic">주기적</string> <string name="onboarding_notifications_mode_service">즉시</string> @@ -945,4 +940,10 @@ <string name="v5_0_app_passcode">앱 패스코드</string> <string name="search_verb">검색</string> <string name="la_mode_off">꺼짐</string> + <string name="abort_switch_receiving_address_confirm">중지하기</string> + <string name="only_owners_can_enable_files_and_media">그룹 소유자만이 파일이나 미디어를 활성화 할 수 있습니다.</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d개의 메시지를 건너뛰었습니다.</string> + <string name="waiting_for_image">이미지 기다리는 중</string> + <string name="voice_message">음성 메시지</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d개의 메시지의 해독에 실패했습니다.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index 1c41913c3e..163645c7d5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -21,7 +21,6 @@ <string name="allow_your_contacts_irreversibly_delete">Leisti jūsų adresatams negrįžtamai ištrinti išsiųstas žinutes.</string> <string name="back">Atgal</string> <string name="settings_section_title_icon">PROGRAMĖLĖS PIKTOGRAMA</string> - <string name="incognito_random_profile_from_contact_description">Adresatui, iš kurio gavote šią nuorodą, bus išsiųstas atsitiktinis profilis</string> <string name="chat_preferences_always">visada</string> <string name="allow_your_contacts_to_send_voice_messages">Leisti jūsų adresatams siųsti balso žinutes.</string> <string name="allow_irreversible_message_deletion_only_if">Leisti negrįžtamą žinučių ištrynimą tik tuo atveju, jei jūsų adresatas jums tai leidžia.</string> @@ -34,7 +33,6 @@ <string name="settings_audio_video_calls">Garso ir vaizdo skambučiai</string> <string name="integrity_msg_bad_hash">bloga žinutės maiša</string> <string name="integrity_msg_bad_id">blogas žinutės ID</string> - <string name="incognito_random_profile_description">Jūsų adresatui bus išsiųstas atsitiktinis profilis</string> <string name="allow_disappearing_messages_only_if">Leisti išnykstančias žinutes tik tuo atveju, jei jūsų adresatas jas leidžia.</string> <string name="clear_chat_warning">Visos žinutės bus ištrintos – to neįmanoma bus atšaukti! Žinutės bus ištrintos TIK jums.</string> <string name="allow_to_delete_messages">Leisti negrįžtamai ištrinti išsiųstas žinutes.</string> @@ -403,7 +401,6 @@ <string name="save_archive">Įrašyti archyvą</string> <string name="button_send_direct_message">Siųsti tiesioginę žinutę</string> <string name="button_remove_member">Šalinti narį</string> - <string name="group_unsupported_incognito_main_profile_sent">Inkognito veiksena čia nepalaikoma – grupės nariams bus išsiųstas jūsų pagrindinis profilis</string> <string name="theme_light">Šviesus</string> <string name="feature_enabled_for_you">įjungta jums</string> <string name="both_you_and_your_contact_can_send_disappearing">Tiek jūs, tiek ir jūsų adresatas gali siųsti išnykstančias žinutes.</string> @@ -530,7 +527,7 @@ <string name="v5_0_large_files_support">Vaizdo įrašai ir failai iki 1GB</string> <string name="search_verb">Ieškoti</string> <string name="la_mode_off">Išjungta</string> - <string name="network_session_mode_user_description">Atskiras TCP ryšys (ir SOCKS kredencialas) bus naudojamas <b>kiekvienam pokalbių profiliui, kurį turite programoje</b>.</string> + <string name="network_session_mode_user_description"><![CDATA[Atskiras TCP ryšys (ir SOCKS kredencialas) bus naudojamas <b>kiekvienam pokalbių profiliui, kurį turite programoje</b>.]]></string> <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">Nepavyko iššifruoti %1$d pranešimų.</string> <string name="button_add_welcome_message">Pridėti sveikinimo pranešimą</string> <string name="v5_2_more_things">Dar keletas dalykų</string> @@ -539,7 +536,7 @@ <string name="always_use_relay">Visada naudoti relę</string> <string name="icon_descr_asked_to_receive">Paprašė leidimo gauti nuotrauką</string> <string name="send_disappearing_message_5_minutes">5 minučių</string> - <string name="onboarding_notifications_mode_off_desc"><b>Mažiausiai bateriją eikvojantis variantas</b>. Jūs gausite sistemos pranešimus tik kai bus atidaryta programėlė (NEBUS fone veikiančios paslaugos).</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Mažiausiai bateriją eikvojantis variantas</b>. Jūs gausite sistemos pranešimus tik kai bus atidaryta programėlė (NEBUS fone veikiančios paslaugos).]]></string> <string name="abort_switch_receiving_address">Atšaukti adreso keitimą</string> <string name="v4_2_auto_accept_contact_requests">Automatiškai priimti susisiekimo užklausas</string> <string name="v5_1_self_destruct_passcode_descr">Kai jį suvedate visi duomenys yra pašalinami.</string> @@ -603,4 +600,20 @@ <string name="one_time_link_short">vienkartinė nuoroda</string> <string name="accept_call_on_lock_screen">Priimti</string> <string name="auto_accept_contact">Automatiškai priimti</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Geriau baterijai</b>. Fono paslauga tikrina pranešimus kas 10 minučių. Galite praleisti skambučius arba skubius pranešimus.]]></string> + <string name="callstatus_in_progress">vyksta skambutis</string> + <string name="icon_descr_cancel_live_message">Atšaukti tiesioginę žinutę</string> + <string name="alert_title_cant_invite_contacts">Nepavyko pakviesti kontaktų!</string> + <string name="icon_descr_call_progress">Vyksta skambutis</string> + <string name="feature_cancelled_item">atšaukta %s</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Galima išjungti nustatymuose</b> - pranešimai vis tiek bus rodomi kol programėlė veikia.]]></string> + <string name="icon_descr_cancel_link_preview">atšaukti nuorodos peržiūrą</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Jūs ir jūsų kontaktas galite pridėti reakcijas į žinutę.</string> + <string name="call_on_lock_screen">Skambučiai užrakinimo ekrane:</string> + <string name="invite_prohibited">Nepavyko pakviesti kontakto!</string> + <string name="v4_5_transport_isolation_descr">Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA).</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]></string> + <string name="cannot_access_keychain">Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį</string> + <string name="icon_descr_cancel_file_preview">Atšaukti failo peržiūrą</string> + <string name="icon_descr_cancel_image_preview">Atšaukti vaizdo peržiūrą</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index 4167a47847..aaa79b9a71 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -48,7 +48,6 @@ <string name="allow_verb">Toestaan</string> <string name="chat_item_ttl_day">1 dag</string> <string name="accept_feature">Accepteer</string> - <string name="incognito_random_profile_from_contact_description">Er wordt een willekeurig profiel verzonden naar het contact van wie je deze link hebt ontvangen</string> <string name="network_session_mode_entity_description">Er wordt een afzonderlijke TCP-verbinding (en SOCKS-referentie) gebruikt <b> voor elk contact en groepslid </b>. \n<b>Let op</b>: als u veel verbindingen heeft, kan uw batterij- en verkeersverbruik aanzienlijk hoger zijn en kunnen sommige verbindingen uitvallen.</string> <string name="icon_descr_audio_call">audio oproep</string> @@ -59,17 +58,16 @@ <string name="back">Terug</string> <string name="v4_2_auto_accept_contact_requests">Contact verzoeken automatisch accepteren</string> <string name="bold_text">vetgedrukt</string> - <string name="incognito_random_profile_description">Er wordt een willekeurig profiel naar uw contactpersoon verzonden</string> <string name="attach">Bijvoegen</string> - <string name="allow_irreversible_message_deletion_only_if">Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contactpersoon dit toestaat.</string> + <string name="allow_irreversible_message_deletion_only_if">Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat.</string> <string name="allow_to_send_disappearing">Sta toe om verdwijnende berichten te verzenden.</string> <string name="allow_your_contacts_to_send_voice_messages">Sta toe dat uw contacten spraak berichten verzenden.</string> <string name="all_your_contacts_will_remain_connected">Al uw contacten blijven verbonden.</string> <string name="allow_voice_messages_question">Spraak berichten toestaan\?</string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Goed voor de batterij</b>. Achtergrondservice controleert berichten elke 10 minuten. Mogelijk mist u oproepen of dringende berichten.]]></string> <string name="integrity_msg_bad_hash">Onjuiste bericht hash</string> - <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR-code</b>: om verbinding te maken met uw contactpersoon die u de QR-code laat zien.]]></string> - <string name="integrity_msg_bad_id">Onjuiste bericht ID</string> + <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>Scan QR-code</b>: om verbinding te maken met uw contact die u de QR-code laat zien.]]></string> + <string name="integrity_msg_bad_id">Onjuiste bericht-ID</string> <string name="call_already_ended">Oproep al beëindigd!</string> <string name="chat_item_ttl_month">1 maand</string> <string name="about_simplex">Over SimpleX</string> @@ -77,8 +75,8 @@ <string name="above_then_preposition_continuation">hier boven, dan:</string> <string name="users_delete_all_chats_deleted">Alle gesprekken en berichten worden verwijderd, dit kan niet ongedaan worden gemaakt!</string> <string name="clear_chat_warning">Alle berichten worden verwijderd, dit kan niet ongedaan worden gemaakt! De berichten worden ALLEEN voor jou verwijderd.</string> - <string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contactpersoon dit toestaat.</string> - <string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contactpersoon ze toestaat.</string> + <string name="allow_disappearing_messages_only_if">Sta verdwijnende berichten alleen toe als uw contact dit toestaat.</string> + <string name="allow_voice_messages_only_if">Sta spraak berichten alleen toe als uw contact ze toestaat.</string> <string name="allow_your_contacts_irreversibly_delete">Laat uw contacten verzonden berichten onomkeerbaar verwijderen.</string> <string name="allow_your_contacts_to_send_disappearing_messages">Sta toe dat uw contacten verdwijnende berichten verzenden.</string> <string name="chat_preferences_always">altijd</string> @@ -101,9 +99,9 @@ <string name="turning_off_service_and_periodic">Batterijoptimalisatie is actief, waardoor achtergrondservice en periodieke verzoeken om nieuwe berichten worden uitgeschakeld. Je kunt ze weer inschakelen via instellingen.</string> <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Het beste voor de batterij</b>. U ontvangt alleen meldingen wanneer de app wordt uitgevoerd (GEEN achtergrondservice).]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Het kan worden uitgeschakeld via instellingen</b>, meldingen worden nog steeds weergegeven terwijl de app actief is.]]></string> - <string name="both_you_and_your_contacts_can_delete">Zowel jij als je contactpersoon kunnen verzonden berichten onherroepelijk verwijderen.</string> - <string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contactpersoon kunnen verdwijnende berichten sturen.</string> - <string name="both_you_and_your_contact_can_send_voice">Zowel jij als je contactpersoon kunnen spraak berichten verzenden.</string> + <string name="both_you_and_your_contacts_can_delete">Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen.</string> + <string name="both_you_and_your_contact_can_send_disappearing">Zowel jij als je contact kunnen verdwijnende berichten sturen.</string> + <string name="both_you_and_your_contact_can_send_voice">Zowel jij als je contact kunnen spraak berichten verzenden.</string> <string name="impossible_to_recover_passphrase"><![CDATA[<b>Let op</b>: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt.]]></string> <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Gebruikt meer batterij</b>! Achtergrondservice wordt altijd uitgevoerd - meldingen worden weergegeven zodra berichten beschikbaar zijn.]]></string> <string name="icon_descr_cancel_link_preview">link voorbeeld annuleren</string> @@ -229,7 +227,7 @@ <string name="group_member_status_announced">verbinden (aangekondigd)</string> <string name="info_row_connection">Verbinding</string> <string name="create_secret_group_title">Maak een geheime groep aan</string> - <string name="info_row_database_id">Database ID</string> + <string name="info_row_database_id">Database-ID</string> <string name="chat_preferences_contact_allows">Contact maakt het mogelijk</string> <string name="contact_preferences">Contact voorkeuren</string> <string name="contacts_can_mark_messages_for_deletion">Contact personen kunnen berichten markeren voor verwijdering; u kunt ze wel bekijken.</string> @@ -376,7 +374,7 @@ <string name="delete_group_for_self_cannot_undo_warning">De groep wordt voor u verwijderd, dit kan niet ongedaan worden gemaakt!</string> <string name="hide_notification">Verbergen</string> <string name="server_error">fout</string> - <string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact persoon online is, even geduld a.u.b. of controleer later!</string> + <string name="file_will_be_received_when_contact_is_online">Het bestand wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of controleer later!</string> <string name="error_saving_file">Fout bij opslaan van bestand</string> <string name="file_not_found">Bestand niet gevonden</string> <string name="file_saved">Bestand opgeslagen</string> @@ -422,7 +420,7 @@ <string name="mark_read">Markeer gelezen</string> <string name="mark_unread">Markeer als ongelezen</string> <string name="mute_chat">Dempen</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het video gesprek </b>, of uw contactpersoon kan een uitnodiging link delen.]]></string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Als u elkaar niet persoonlijk kunt ontmoeten, kunt u <b> de QR-code scannen in het video gesprek </b>, of uw contact kan een uitnodiging link delen.]]></string> <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Als je elkaar niet persoonlijk kunt ontmoeten, <b>toon je de QR-code in het video gesprek</b> of deel je de link.]]></string> <string name="invalid_QR_code">Ongeldige QR-code</string> <string name="icon_descr_more_button">Meer</string> @@ -437,7 +435,7 @@ <string name="how_it_works">Hoe het werkt</string> <string name="callstatus_missed">gemiste oproep</string> <string name="how_simplex_works">Hoe SimpleX werkt</string> - <string name="many_people_asked_how_can_it_deliver"><![CDATA[Veel mensen vroegen: <i>als SimpleX geen gebruikers ID\'s heeft, hoe kan het dan berichten bezorgen\?</i>]]></string> + <string name="many_people_asked_how_can_it_deliver"><![CDATA[Veel mensen vroegen: <i>als SimpleX geen gebruikers-ID\'s heeft, hoe kan het dan berichten bezorgen\?</i>]]></string> <string name="incoming_audio_call">Inkomende audio oproep</string> <string name="incoming_video_call">Inkomend video gesprek</string> <string name="ignore">Negeren</string> @@ -451,7 +449,6 @@ <string name="group_member_status_invited">uitgenodigd</string> <string name="button_leave_group">Groep verlaten</string> <string name="info_row_local_name">Lokale naam</string> - <string name="group_unsupported_incognito_main_profile_sent">Incognito modus wordt hier niet ondersteund, uw hoofdprofiel wordt naar groepsleden verzonden</string> <string name="users_delete_data_only">Alleen lokale profielgegevens</string> <string name="message_deletion_prohibited_in_chat">Het onomkeerbaar verwijderen van berichten is verboden in deze groep.</string> <string name="v4_3_improved_privacy_and_security_desc">App scherm verbergen in de recente apps.</string> @@ -472,7 +469,7 @@ <string name="v4_5_reduced_battery_usage_descr">Meer verbeteringen volgen snel!</string> <string name="button_add_members">Nodig leden uit</string> <string name="notification_display_mode_hidden_desc">Verberg contact en bericht</string> - <string name="turn_off_battery_optimization"><![CDATA[Om het te gebruiken <b>batterijoptimalisatie uitschakelen</b> voor SimpleX in het volgende dialoogvenster. Anders worden de meldingen uitgeschakeld.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Om het te gebruiken, kunt u Simplex in het volgende dialoogvenster \u0020<b>op de achtergrond uitvoeren</b>. Anders worden de meldingen uitgeschakeld.]]></string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Als u ervoor kiest om te weigeren, wordt de afzender NIET op de hoogte gesteld.</string> <string name="onboarding_notifications_mode_service">Onmiddellijk</string> <string name="rcv_group_event_member_added">heeft %1$s uitgenodigd</string> @@ -482,7 +479,7 @@ <string name="group_preview_join_as">lid worden als %s</string> <string name="alert_text_skipped_messages_it_can_happen_when">Het kan gebeuren wanneer: \n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server. -\n2. Decodering van het bericht is mislukt, omdat u of uw contactpersoon een oude databaseback-up heeft gebruikt. +\n2. Decodering van het bericht is mislukt, omdat u of uw contact een oude databaseback-up heeft gebruikt. \n3. De verbinding is verbroken.</string> <string name="joining_group">Deel nemen aan groep</string> <string name="leave_group_button">Verlaten</string> @@ -501,7 +498,7 @@ <string name="import_database">Database importeren</string> <string name="v4_3_improved_privacy_and_security">Verbeterde privacy en veiligheid</string> <string name="image_will_be_received_when_contact_is_online">De afbeelding wordt ontvangen wanneer uw contact online is, even geduld a.u.b. of kijk later!</string> - <string name="incognito_info_protects">De incognito modus beschermt de privacy van uw hoofdprofielnaam en afbeelding, voor elk nieuw contact wordt een nieuw willekeurig profiel gemaakt.</string> + <string name="incognito_info_protects">Incognito beschermt je privacy door een nieuw willekeurig profiel te gebruiken voor elk contact.</string> <string name="new_database_archive">Nieuw database archief</string> <string name="no_details">geen details</string> <string name="conn_level_desc_indirect">indirect (%1$s)</string> @@ -518,7 +515,7 @@ <string name="network_and_servers">Netwerk & servers</string> <string name="enter_one_ICE_server_per_line">ICE servers (één per lijn)</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string> - <string name="network_disable_socks_info">Als u bevestigt, kunnen de berichten servers uw IP-adres zien en uw provider, met welke servers u verbinding maakt.</string> + <string name="network_disable_socks_info">Als u bevestigt, kunnen de berichten servers en uw provider uw IP-adres zien en met welke servers u verbinding maakt.</string> <string name="network_use_onion_hosts_no">Nee</string> <string name="immune_to_spam_and_abuse">Immuun voor spam en misbruik</string> <string name="make_private_connection">Maak een privéverbinding</string> @@ -530,7 +527,7 @@ <string name="rcv_group_event_invited_via_your_group_link">uitgenodigd via je groep link</string> <string name="incognito">Incognito</string> <string name="icon_descr_call_missed">Gemiste oproep</string> - <string name="description_via_contact_address_link_incognito">incognito via contact adres link</string> + <string name="description_via_contact_address_link_incognito">incognito via contactadres link</string> <string name="description_via_group_link_incognito">incognito via groep link</string> <string name="description_via_one_time_link_incognito">incognito via eenmalige link</string> <string name="invalid_chat">ongeldige gesprek</string> @@ -540,7 +537,7 @@ <string name="live">LIVE</string> <string name="ensure_smp_server_address_are_correct_format_and_unique">Zorg ervoor dat SMP server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn.</string> <string name="marked_deleted_description">gemarkeerd als verwijderd</string> - <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contactpersoon om u een andere te sturen.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen.</string> <string name="image_descr_profile_image">profielfoto</string> <string name="privacy_redefined">Privacy opnieuw gedefinieerd</string> <string name="privacy_and_security">Privacy en beveiliging</string> @@ -566,8 +563,8 @@ <string name="chat_preferences_off">uit</string> <string name="chat_preferences_on">aan</string> <string name="only_you_can_send_disappearing">Alleen jij kunt verdwijnende berichten verzenden.</string> - <string name="only_your_contact_can_send_disappearing">Alleen uw contactpersoon kan verdwijnende berichten verzenden.</string> - <string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contactpersoon kan ze markeren voor verwijdering).</string> + <string name="only_your_contact_can_send_disappearing">Alleen uw contact kan verdwijnende berichten verzenden.</string> + <string name="only_you_can_delete_messages">Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering).</string> <string name="feature_offered_item_with_param">voorgesteld %s: %2s</string> <string name="old_database_archive">Oud database archief</string> <string name="enter_correct_current_passphrase">Voer het juiste huidige wachtwoord in.</string> @@ -583,12 +580,11 @@ <string name="network_use_onion_hosts_required_desc_in_alert">Onion hosts zijn vereist voor verbinding.</string> <string name="only_group_owners_can_change_prefs">Alleen groep eigenaren kunnen groep voorkeuren wijzigen.</string> <string name="only_stored_on_members_devices">(alleen opgeslagen door groepsleden)</string> - <string name="paste_connection_link_below_to_connect">Plak de link die je hebt ontvangen in het vak hieronder om verbinding te maken met je contactpersoon.</string> <string name="smp_servers_preset_server">Vooraf ingestelde server</string> <string name="periodic_notifications_disabled">Periodieke meldingen zijn uitgeschakeld!</string> <string name="icon_descr_server_status_pending">In behandeling</string> <string name="only_group_owners_can_enable_voice">Alleen groep eigenaren kunnen spraak berichten inschakelen.</string> - <string name="ask_your_contact_to_enable_voice">Vraag uw contactpersoon om het verzenden van spraak berichten in te schakelen.</string> + <string name="ask_your_contact_to_enable_voice">Vraag uw contact om het verzenden van spraak berichten in te schakelen.</string> <string name="ok">OK</string> <string name="network_use_onion_hosts_required_desc">Onion hosts zijn vereist voor verbinding.</string> <string name="network_use_onion_hosts_prefer_desc">Onion hosts worden gebruikt indien beschikbaar.</string> @@ -596,9 +592,9 @@ <string name="network_use_onion_hosts_no_desc">Onion hosts worden niet gebruikt.</string> <string name="opensource_protocol_and_code_anybody_can_run_servers">Open-source protocol en code. Iedereen kan de servers draaien.</string> <string name="people_can_connect_only_via_links_you_share">Mensen kunnen alleen verbinding met u maken via de links die u deelt.</string> - <string name="only_your_contact_can_delete">Alleen uw contactpersoon kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string> + <string name="only_your_contact_can_delete">Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering).</string> <string name="only_you_can_send_voice">Alleen jij kunt spraak berichten verzenden.</string> - <string name="only_your_contact_can_send_voice">Alleen uw contactpersoon kan spraak berichten verzenden.</string> + <string name="only_your_contact_can_send_voice">Alleen uw contact kan spraak berichten verzenden.</string> <string name="prohibit_message_deletion">Verbied het onomkeerbaar verwijderen van berichten.</string> <string name="feature_offered_item">voorgesteld %s</string> <string name="store_passphrase_securely_without_recover">Sla het wachtwoord veilig op. Als u deze kwijtraakt, heeft u GEEN toegang tot de gesprekken.</string> @@ -645,7 +641,7 @@ <string name="simplex_service_notification_title">SimpleX Chat service</string> <string name="simplex_service_notification_text">Berichten ontvangen…</string> <string name="notification_preview_mode_message_desc">Toon contact en bericht</string> - <string name="notification_preview_mode_contact_desc">Toon alleen contactpersoon</string> + <string name="notification_preview_mode_contact_desc">Toon alleen contact</string> <string name="ntf_channel_messages">SimpleX Chat berichten</string> <string name="auth_simplex_lock_turned_on">SimpleX Vergrendelen ingeschakeld</string> <string name="auth_stop_chat">Stop chat</string> @@ -678,30 +674,29 @@ <string name="connect_via_link_or_qr_from_clipboard_or_in_person">(scannen of plakken vanaf klembord)</string> <string name="to_connect_via_link_title">Om verbinding te maken via een link</string> <string name="reject_contact_button">Afwijzen</string> - <string name="set_contact_name">Naam contactpersoon instellen</string> + <string name="set_contact_name">Naam contact instellen</string> <string name="connection_you_accepted_will_be_cancelled">De door u geaccepteerde verbinding wordt geannuleerd!</string> - <string name="contact_you_shared_link_with_wont_be_able_to_connect">Het contact met wie je deze link hebt gedeeld, kan GEEN verbinding maken!</string> + <string name="contact_you_shared_link_with_wont_be_able_to_connect">Het contact met wie je deze link hebt gedeeld kan GEEN verbinding maken!</string> <string name="you_accepted_connection">Je hebt de verbinding geaccepteerd</string> - <string name="you_invited_your_contact">Je hebt je contactpersoon uitgenodigd</string> - <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Uw contactpersoon moet online zijn om de verbinding te voltooien. -\nU kunt deze verbinding verbreken en het contact verwijderen (en later proberen met een nieuwe link).</string> + <string name="you_invited_a_contact">Je hebt je contact uitgenodigd</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Uw contact moet online zijn om de verbinding te voltooien. +\nU kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link.</string> <string name="image_descr_qr_code">QR code</string> <string name="icon_descr_settings">Instellingen</string> <string name="contact_wants_to_connect_with_you">wil met je in contact komen!</string> <string name="icon_descr_address">SimpleX Adres</string> <string name="show_QR_code">Toon QR-code</string> <string name="image_descr_simplex_logo">SimpleX-Logo</string> - <string name="your_chat_profile_will_be_sent_to_your_contact">Je chat profiel wordt verzonden naar uw contactpersoon</string> + <string name="your_chat_profile_will_be_sent_to_your_contact">Je chat profiel wordt verzonden naar uw contact</string> <string name="you_will_be_connected_when_group_host_device_is_online">Je wordt verbonden met de groep wanneer het apparaat van de groep host online is, even geduld a.u.b. of controleer het later!</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">U wordt verbonden wanneer uw verbindingsverzoek wordt geaccepteerd, even geduld a.u.b. of controleer later!</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Je wordt verbonden wanneer het apparaat van je contact online is, even geduld a.u.b. of controleer het later!</string> - <string name="scan_code_from_contacts_app">Scan de beveiligingscode van de app van uw contactpersoon.</string> + <string name="scan_code_from_contacts_app">Scan de beveiligingscode van de app van uw contact.</string> <string name="security_code">Beveiligingscode</string> <string name="is_not_verified">%s is niet geverifieerd</string> <string name="is_verified">%s is geverifieerd</string> - <string name="to_verify_compare">Vergelijk (of scan) de code op uw apparaten om end-to-end codering met uw contactpersoon te verifiëren.</string> + <string name="to_verify_compare">Vergelijk (of scan) de code op uw apparaten om end-to-end codering met uw contact te verifiëren.</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[U kunt ook verbinding maken door op de link te klikken. Als het in de browser wordt geopend, klikt u op de knop <b> Openen in mobiele app </b>.]]></string> - <string name="your_profile_will_be_sent">Uw chat profiel wordt naar uw contactpersoon verzonden</string> <string name="your_chat_profiles">Uw chat profielen</string> <string name="your_simplex_contact_address">Uw SimpleX adres</string> <string name="chat_with_the_founder">Stuur vragen en ideeën</string> @@ -733,7 +728,7 @@ <string name="secret_text">geheim</string> <string name="next_generation_of_private_messaging">De volgende generatie privéberichten</string> <string name="callstate_waiting_for_answer">wachten op antwoord…</string> - <string name="to_protect_privacy_simplex_has_ids_for_queues">Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichten wachtrijen, afzonderlijk voor elk van uw contacten.</string> <string name="use_chat">Gebruik chat</string> <string name="onboarding_notifications_mode_off">Wanneer de app actief is</string> <string name="video_call_no_encryption">video gesprek (niet e2e versleuteld)</string> @@ -820,7 +815,6 @@ <string name="network_options_revert">Terugdraaien</string> <string name="network_options_save">Opslaan</string> <string name="network_option_seconds_label">sec</string> - <string name="incognito_info_find">Om het profiel te vinden dat wordt gebruikt voor een incognito verbinding, tikt u op de naam van het contact of de groep bovenaan het gesprek.</string> <string name="incognito_info_share">Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen.</string> <string name="theme">Thema</string> <string name="reset_color">Kleuren resetten</string> @@ -870,7 +864,7 @@ <string name="update_network_session_mode_question">Transportisolatiemodus updaten\?</string> <string name="callstate_received_confirmation">bevestiging ontvangen…</string> <string name="callstate_waiting_for_confirmation">Wachten op bevestiging…</string> - <string name="first_platform_without_user_ids">Het eerste platform zonder gebruikers ID\'s, privé door ontwerp.</string> + <string name="first_platform_without_user_ids">Het eerste platform zonder gebruikers-ID\'s, privé door ontwerp.</string> <string name="prohibit_direct_messages">Verbied het sturen van directe berichten naar leden.</string> <string name="prohibit_sending_voice">Verbieden het verzenden van spraak berichten.</string> <string name="v4_2_security_assessment_desc">De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits.</string> @@ -913,8 +907,8 @@ <string name="chat_preferences_you_allow">Jij staat toe</string> <string name="you_are_invited_to_group">Je bent uitgenodigd voor de groep</string> <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[U kunt <font color="#0088ff">verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen</font>.]]></string> - <string name="connection_error_auth_desc">Tenzij uw contactpersoon de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. -\nOm verbinding te maken, vraagt u uw contactpersoon om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string> + <string name="connection_error_auth_desc">Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. +\nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft.</string> <string name="update_onion_hosts_settings_question">.onion hosts-instelling updaten\?</string> <string name="use_simplex_chat_servers__question">SimpleX Chat servers gebruiken\?</string> <string name="voice_messages_are_prohibited">Spraak berichten zijn verboden in deze groep.</string> @@ -922,10 +916,10 @@ <string name="you_can_start_chat_via_setting_or_by_restarting_the_app">U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten.</string> <string name="snd_conn_event_switch_queue_phase_completed_for_member">je hebt het adres gewijzigd voor %s</string> <string name="snd_group_event_member_deleted">je hebt %1$s verwijderd</string> - <string name="contact_sent_large_file">Je contactpersoon heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s).</string> + <string name="contact_sent_large_file">Je contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%1$s).</string> <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Uw huidige chatdatabase wordt VERWIJDERD en VERVANGEN door de geïmporteerde. \nDeze actie kan niet ongedaan worden gemaakt. Uw profiel, contacten, berichten en bestanden gaan onomkeerbaar verloren.</string> - <string name="invite_prohibited_description">Je probeert een contact met wie je een incognito profiel hebt gedeeld, uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</string> + <string name="invite_prohibited_description">Je probeert een contact met wie je een incognito profiel hebt gedeeld uit te nodigen voor de groep waarin je je hoofdprofiel gebruikt</string> <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Uw profiel wordt op uw apparaat opgeslagen en alleen gedeeld met uw contacten. SimpleX servers kunnen uw profiel niet zien.</string> <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">U moet zich authenticeren wanneer u de app na 30 seconden op de achtergrond start of hervat.</string> <string name="images_limit_title">Te veel afbeeldingen!</string> @@ -943,7 +937,7 @@ <string name="trying_to_connect_to_server_to_receive_messages_with_error">Er wordt geprobeerd verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %1$s).</string> <string name="unknown_message_format">onbekend berichtformaat</string> <string name="simplex_link_mode_browser">Via browser</string> - <string name="description_via_contact_address_link">via contact adres link</string> + <string name="description_via_contact_address_link">via contactadres link</string> <string name="description_via_group_link">via groep link</string> <string name="description_via_one_time_link">via een eenmalige link</string> <string name="simplex_link_connection">via %1$s</string> @@ -1020,10 +1014,10 @@ <string name="confirm_database_upgrades">Bevestig database upgrades</string> <string name="mtr_error_no_down_migration">database versie is nieuwer dan de app, maar geen down migratie voor: %s</string> <string name="incompatible_database_version">Incompatibele database versie</string> - <string name="file_will_be_received_when_contact_completes_uploading">Het bestand wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</string> - <string name="image_will_be_received_when_contact_completes_uploading">De afbeelding wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</string> + <string name="file_will_be_received_when_contact_completes_uploading">Het bestand wordt gedownload wanneer uw contact het uploaden heeft voltooid.</string> + <string name="image_will_be_received_when_contact_completes_uploading">De afbeelding wordt gedownload wanneer uw contact het uploaden heeft voltooid.</string> <string name="show_dev_options">Toon:</string> - <string name="developer_options">Database ID\'s en Transport isolatie optie.</string> + <string name="developer_options">Database-ID\'s en Transport isolatie optie.</string> <string name="hide_dev_options">Verbergen:</string> <string name="show_developer_options">Ontwikkelaars opties tonen</string> <string name="settings_section_title_experimenta">EXPERIMENTEEL</string> @@ -1040,7 +1034,7 @@ <string name="icon_descr_waiting_for_video">Wachten op video</string> <string name="waiting_for_video">Wachten op video</string> <string name="video_descr">Video</string> - <string name="video_will_be_received_when_contact_completes_uploading">De video wordt ontvangen wanneer uw contactpersoon het uploaden heeft voltooid.</string> + <string name="video_will_be_received_when_contact_completes_uploading">De video wordt gedownload wanneer uw contact het uploaden heeft voltooid.</string> <string name="error_saving_xftp_servers">Fout bij opslaan van XFTP-servers</string> <string name="error_loading_xftp_servers">Fout bij het laden van XFTP servers</string> <string name="error_xftp_test_server_auth">Server vereist autorisatie om te uploaden, wachtwoord controleren</string> @@ -1092,13 +1086,13 @@ <string name="decryption_error">Decodering fout</string> <string name="alert_text_msg_bad_hash">De hash van het vorige bericht is anders.</string> <string name="alert_text_decryption_error_too_many_skipped">%1$d berichten overgeslagen.</string> - <string name="alert_text_fragment_encryption_out_of_sync_old_database">Het kan gebeuren wanneer u of uw verbinding de oude databaseback-up gebruikte.</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Het kan gebeuren wanneer u of de ander een oude databaseback-up gebruikt.</string> <string name="alert_text_fragment_please_report_to_developers">Meld het alsjeblieft aan de ontwikkelaars.</string> <string name="alert_title_msg_bad_hash">Onjuiste bericht hash</string> - <string name="alert_title_msg_bad_id">Onjuiste bericht ID</string> + <string name="alert_title_msg_bad_id">Onjuiste bericht-ID</string> <string name="alert_text_msg_bad_id">De ID van het volgende bericht is onjuist (minder of gelijk aan het vorige). \nHet kan gebeuren vanwege een bug of wanneer de verbinding is aangetast.</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d-berichten konden niet worden ontsleuteld.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d berichten konden niet worden ontsleuteld.</string> <string name="no_spaces">Geen spaties!</string> <string name="stop_rcv_file__message">Het ontvangen van het bestand wordt gestopt.</string> <string name="revoke_file__confirm">Intrekken</string> @@ -1110,10 +1104,10 @@ <string name="stop_snd_file__title">Bestand verzenden stoppen\?</string> <string name="only_your_contact_can_make_calls">Alleen je contact kan bellen.</string> <string name="revoke_file__message">Het bestand wordt van de servers verwijderd.</string> - <string name="both_you_and_your_contact_can_make_calls">Zowel u als uw contact persoon kunnen bellen.</string> + <string name="both_you_and_your_contact_can_make_calls">Zowel u als uw contact kunnen bellen.</string> <string name="only_you_can_make_calls">Alleen jij kunt bellen.</string> <string name="stop_snd_file__message">Het verzenden van het bestand wordt gestopt.</string> - <string name="allow_calls_only_if">Sta oproepen alleen toe als uw contact persoon dit toestaat.</string> + <string name="allow_calls_only_if">Sta oproepen alleen toe als uw contact dit toestaat.</string> <string name="allow_your_contacts_to_call">Sta toe dat uw contacten u bellen.</string> <string name="audio_video_calls">Audio/video oproepen</string> <string name="available_in_v51">" @@ -1129,7 +1123,7 @@ <string name="auth_open_chat_profiles">Chat profielen openen</string> <string name="learn_more_about_address">Over SimpleX adres</string> <string name="learn_more">Kom meer te weten</string> - <string name="scan_qr_to_connect_to_contact">Om verbinding te maken, kan uw contact persoon de QR-code scannen of de link in de app gebruiken.</string> + <string name="scan_qr_to_connect_to_contact">Om verbinding te maken, kan uw contact de QR-code scannen of de link in de app gebruiken.</string> <string name="customize_theme_title">Thema aanpassen</string> <string name="group_welcome_preview">Voorbeeld</string> <string name="you_can_share_this_address_with_your_contacts">U kunt dit adres delen met uw contacten om ze verbinding te laten maken met %s.</string> @@ -1181,7 +1175,7 @@ <string name="read_more_in_user_guide_with_link"><![CDATA[Lees meer in de <font color="#0088ff">Gebruikershandleiding</font>.]]></string> <string name="theme_colors_section_title">THEMA KLEUREN</string> <string name="you_can_share_your_address">U kunt uw adres delen als een link of QR-code - iedereen kan verbinding met u maken.</string> - <string name="your_contacts_will_see_it">Uw contacten in SimpleX zullen het zien. + <string name="your_contacts_will_see_it">Uw contacten in SimpleX kunnen het zien. \nU kunt dit wijzigen in Instellingen.</string> <string name="all_app_data_will_be_cleared">Alle app-gegevens worden verwijderd.</string> <string name="empty_chat_profile_is_created">Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend.</string> @@ -1202,14 +1196,14 @@ <string name="only_you_can_add_message_reactions">Alleen jij kunt berichtreacties toevoegen.</string> <string name="message_reactions_are_prohibited">Reacties op berichten zijn verboden in deze groep.</string> <string name="prohibit_message_reactions_group">Berichten reacties verbieden.</string> - <string name="allow_message_reactions_only_if">Sta berichtreacties alleen toe als uw contactpersoon dit toestaat.</string> + <string name="allow_message_reactions_only_if">Sta berichtreacties alleen toe als uw contact dit toestaat.</string> <string name="allow_your_contacts_adding_message_reactions">Sta uw contactpersonen toe om berichtreacties toe te voegen.</string> <string name="allow_message_reactions">Sta berichtreacties toe.</string> <string name="group_members_can_add_message_reactions">Groepsleden kunnen berichtreacties toevoegen.</string> - <string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contactpersoon kunnen berichtreacties toevoegen.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Zowel u als uw contact kunnen berichtreacties toevoegen.</string> <string name="message_reactions">Reacties op berichten</string> <string name="message_reactions_prohibited_in_this_chat">Reacties op berichten zijn verboden in deze chat.</string> - <string name="only_your_contact_can_add_message_reactions">Alleen uw contactpersoon kan berichtreacties toevoegen.</string> + <string name="only_your_contact_can_add_message_reactions">Alleen uw contact kan berichtreacties toevoegen.</string> <string name="custom_time_unit_days">dagen</string> <string name="custom_time_unit_hours">uren</string> <string name="custom_time_unit_minutes">minuten</string> @@ -1230,9 +1224,9 @@ <string name="item_info_current">(huidig)</string> <string name="share_text_database_id">Database-ID: %d</string> <string name="info_menu">Info</string> - <string name="info_row_disappears_at">Verdwijnt om</string> + <string name="info_row_disappears_at">Verdwijnt op</string> <string name="share_text_moderated_at">Gemodereerd op: %s</string> - <string name="share_text_disappears_at">Verdwijnt om: %s</string> + <string name="share_text_disappears_at">Verdwijnt op: %s</string> <string name="current_version_timestamp">%s (huidig)</string> <string name="share_text_sent_at">Verzonden op: %s</string> <string name="error_loading_details">Fout bij het laden van details</string> @@ -1307,21 +1301,21 @@ <string name="conn_event_ratchet_sync_allowed">versleuteling heronderhandeling toegestaan</string> <string name="error_synchronizing_connection">Fout bij het synchroniseren van de verbinding</string> <string name="sync_connection_force_desc">De versleuteling werkt en de nieuwe versleutelingsovereenkomst is niet vereist. Dit kan leiden tot verbindingsfouten!</string> - <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Het verzenden van ontvangstbevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen.</string> - <string name="v5_2_message_delivery_receipts">Ontvangstbevestiging voor berichten!</string> - <string name="receipts_section_description_1">Ze kunnen worden overschreven in contactinstellingen</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contacten in alle zichtbare chatprofielen.</string> + <string name="v5_2_message_delivery_receipts">Ontvangst bevestiging voor berichten!</string> + <string name="receipts_section_description_1">Ze kunnen worden overschreven in contact en groep instellingen</string> <string name="receipts_section_contacts">Contacten</string> - <string name="receipts_contacts_title_disable">Ontvangstbevestiging uitschakelen\?</string> - <string name="receipts_contacts_title_enable">Ontvangstbevestiging inschakelen\?</string> - <string name="receipts_contacts_override_enabled">Het verzenden van ontvangstbevestiging is ingeschakeld voor %d-contactpersonen</string> + <string name="receipts_contacts_title_disable">Ontvangst bevestiging uitschakelen\?</string> + <string name="receipts_contacts_title_enable">Ontvangst bevestiging inschakelen\?</string> + <string name="receipts_contacts_override_enabled">Het verzenden van ontvangst bevestiging is ingeschakeld voor %d-contactpersonen</string> <string name="receipts_section_description">Deze instellingen gelden voor uw huidige profiel</string> <string name="receipts_contacts_disable_keep_overrides">Uitschakelen (overschrijvingen behouden)</string> <string name="receipts_contacts_enable_for_all">Inschakelen voor iedereen</string> <string name="receipts_contacts_enable_keep_overrides">Inschakelen (overschrijvingen behouden)</string> - <string name="receipts_contacts_override_disabled">Het verzenden van ontvangstbevestiging is uitgeschakeld voor %d-contactpersonen</string> + <string name="receipts_contacts_override_disabled">Het verzenden van ontvangst bevestiging is uitgeschakeld voor %d-contactpersonen</string> <string name="receipts_contacts_disable_for_all">Uitschakelen voor iedereen</string> - <string name="settings_section_title_delivery_receipts">STUUR ONTVANGSTBEVESTIGING NAAR</string> - <string name="send_receipts">Ontvangstbevestiging verzenden</string> + <string name="settings_section_title_delivery_receipts">STUUR ONTVANGST BEVESTIGING NAAR</string> + <string name="send_receipts">Ontvangst bevestiging verzenden</string> <string name="v5_2_message_delivery_receipts_descr">De tweede vink die we gemist hebben! ✅</string> <string name="v5_2_favourites_filter_descr">Filter ongelezen en favoriete chats.</string> <string name="v5_2_favourites_filter">Vind gesprekken sneller</string> @@ -1329,16 +1323,82 @@ <string name="v5_2_fix_encryption">Behoud uw verbindingen</string> <string name="v5_2_disappear_one_message">Eén bericht laten verdwijnen</string> <string name="v5_2_more_things">Nog een paar dingen</string> - <string name="delivery_receipts_are_disabled">Ontvangstbevestiging is uitgeschakeld!</string> - <string name="delivery_receipts_title">Ontvangstbevestiging!</string> + <string name="delivery_receipts_are_disabled">Ontvangst bevestiging is uitgeschakeld!</string> + <string name="delivery_receipts_title">Ontvangst bevestiging!</string> <string name="dont_enable_receipts">Niet inschakelen</string> <string name="enable_receipts_all">Inschakelen</string> <string name="v5_2_disappear_one_message_descr">Zelfs wanneer uitgeschakeld in het gesprek.</string> <string name="v5_2_more_things_descr">- stabielere berichtbezorging. \n- een beetje betere groepen. \n- en meer!</string> - <string name="sending_delivery_receipts_will_be_enabled">Het verzenden van ontvangstbevestiging wordt ingeschakeld voor alle contactpersonen.</string> + <string name="sending_delivery_receipts_will_be_enabled">Het verzenden van ontvangst bevestiging wordt ingeschakeld voor alle contactpersonen.</string> <string name="you_can_enable_delivery_receipts_later">U kunt later inschakelen via Instellingen</string> <string name="you_can_enable_delivery_receipts_later_alert">U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app.</string> - <string name="error_enabling_delivery_receipts">Fout bij het inschakelen van ontvangstbevestiging!</string> + <string name="error_enabling_delivery_receipts">Fout bij het inschakelen van ontvangst bevestiging!</string> + <string name="choose_file_title">Kies een bestand</string> + <string name="in_developing_title">Binnenkort beschikbaar!</string> + <string name="in_developing_desc">Deze functie wordt nog niet ondersteund. Probeer de volgende versie.</string> + <string name="no_selected_chat">Geen geselecteerd gesprek</string> + <string name="delivery">Levering</string> + <string name="no_info_on_delivery">Geen leveringsinformatie</string> + <string name="receipts_groups_title_disable">Ontvangstbevestiging voor groepen uitschakelen\?</string> + <string name="receipts_groups_title_enable">Ontvangstbevestiging inschakelen voor groepen\?</string> + <string name="receipts_section_groups">Kleine groepen (max 20)</string> + <string name="receipts_groups_disable_for_all">Uitschakelen voor alle groepen</string> + <string name="receipts_groups_disable_keep_overrides">Uitschakelen (groep overschrijven behouden)</string> + <string name="receipts_groups_enable_for_all">Inschakelen voor alle groepen</string> + <string name="receipts_groups_enable_keep_overrides">Inschakelen (groep overschrijven behouden)</string> + <string name="receipts_groups_override_disabled">Het verzenden van ontvangstbevestigingen is uitgeschakeld voor %d-groepen</string> + <string name="receipts_groups_override_enabled">Het verzenden van ontvangstbevestigingen is ingeschakeld voor %d-groepen</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled_alert_msg">Deze groep heeft meer dan %1$d leden, Ontvangstbevestigingen worden niet verzonden.</string> + <string name="send_receipts_disabled">uitgeschakeld</string> + <string name="send_receipts_disabled_alert_title">Ontvangstbevestigingen zijn uitgeschakeld</string> + <string name="connect_via_member_address_alert_title">Verbind direct\?</string> + <string name="turn_off_battery_optimization_button">Toestaan</string> + <string name="connect_via_link_incognito">Verbind incognito</string> + <string name="disable_notifications_button">Meldingen uitzetten</string> + <string name="turn_off_system_restriction_button">Open app instellingen</string> + <string name="connect__a_new_random_profile_will_be_shared">Een nieuw willekeurig profiel zal gedeeld worden.</string> + <string name="connect_use_current_profile">Gebruik het huidige profiel</string> + <string name="system_restricted_background_in_call_title">Geen achtergrondgesprekken</string> + <string name="system_restricted_background_desc">SimpleX kan niet op de achtergrond draaien. Je krijgt alleen de berichten als de app open is.</string> + <string name="system_restricted_background_in_call_desc">De app is misschien afgesloten na 1 minuut achtergrond.</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Kies de <b>app -batterijgebruik</b> / <b>onbeperkt</b> in de app -instellingen om met gesloten app te bellen.]]></string> + <string name="connect__your_profile_will_be_shared">Uw profiel %1$s wordt gedeeld.</string> + <string name="connect_via_member_address_alert_desc">Verzoek voor het verbinden wordt naar dit groepslid verzonden.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Plak de link die je hebt ontvangen om verbinding te maken met je contact…</string> + <string name="system_restricted_background_warn"><![CDATA[Als u meldingen wilt inschakelen, kiest u <b> App-batterijgebruik</b> /<b> Onbeperkt</b> in de app-instellingen.]]></string> + <string name="connect_use_new_incognito_profile">Gebruik een nieuw incognito profiel</string> + <string name="privacy_message_draft">Concept bericht</string> + <string name="rcv_group_event_n_members_connected">%s, %s en %d andere leden verbonden</string> + <string name="privacy_show_last_messages">Laat laatste berichten zien</string> + <string name="rcv_group_event_3_members_connected">%s, %s en %s verbonden</string> + <string name="rcv_group_event_2_members_connected">%s en %s verbonden</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">De database wordt versleuteld en het wachtwoord wordt opgeslagen in de instellingen.</string> + <string name="you_can_change_it_later">Willekeurig wachtwoord wordt in de instellingen opgeslagen als platte tekst. +\nJe kunt het later wijzigen.</string> + <string name="database_encryption_will_be_updated_in_settings">Het wachtwoord voor database versleuteling wordt bijgewerkt en opgeslagen in de instellingen.</string> + <string name="remove_passphrase_from_settings">Wachtwoord uit instellingen verwijderen\?</string> + <string name="use_random_passphrase">Gebruik een willekeurig wachtwoord</string> + <string name="save_passphrase_in_settings">Bewaar het wachtwoord in de instellingen</string> + <string name="setup_database_passphrase">Database wachtwoord instellen</string> + <string name="set_database_passphrase">Database wachtwoord instellen</string> + <string name="open_database_folder">Database map openen</string> + <string name="passphrase_will_be_saved_in_settings">Het wachtwoord wordt als platte tekst in de instellingen opgeslagen nadat u deze hebt gewijzigd of de app opnieuw hebt opgestart.</string> + <string name="settings_is_storing_in_clear_text">Het wachtwoord wordt als leesbare tekst in de instellingen opgeslagen.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Let op</b>: bericht en bestands relais zijn verbonden via SOCKS-proxy. Voor oproepen en het verzenden van link voorbeelden wordt gebruik gemaakt van een directe verbinding.]]></string> + <string name="encrypt_local_files">Versleutel lokale bestanden</string> + <string name="v5_3_encrypt_local_files">Versleutel opgeslagen bestanden en media</string> + <string name="v5_3_new_desktop_app">Nieuwe desktop app!</string> + <string name="v5_3_new_interface_languages">6 nieuwe interfacetalen</string> + <string name="v5_3_encrypt_local_files_descr">App versleutelt nieuwe lokale bestanden (behalve video\'s)</string> + <string name="v5_3_discover_join_groups">Ontdek en sluit je aan bij groepen</string> + <string name="v5_3_simpler_incognito_mode">Vereenvoudigde incognitomodus</string> + <string name="v5_3_new_interface_languages_descr">Arabisch, Bulgaars, Fins, Hebreeuws, Thais en Oekraïens - dankzij de gebruikers en Weblate.</string> + <string name="v5_3_new_desktop_app_descr">Maak een nieuw profiel in de desktop-app. 💻</string> + <string name="v5_3_simpler_incognito_mode_descr">Schakel incognito in tijdens het verbinden.</string> + <string name="v5_3_discover_join_groups_descr">- maak verbinding met de directoryservice (BETA)! +\n- ontvangst bevestiging (tot 20 leden). +\n- sneller en stabieler.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index 3364d15f14..35630919e6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -257,7 +257,7 @@ <string name="unmute_chat">Wyłącz wyciszenie</string> <string name="contact_wants_to_connect_with_you">chce się z Tobą połączyć!</string> <string name="you_accepted_connection">Zaakceptowałeś połączenie</string> - <string name="you_invited_your_contact">Zaprosiłeś swój kontakt</string> + <string name="you_invited_a_contact">Zaprosiłeś swój kontakt</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Twój kontakt musi być online, aby połączenie zostało zakończone. \nMożesz anulować to połączenie i usunąć kontakt (i spróbować później z nowym linkiem).</string> <string name="connect_button">Połącz</string> @@ -273,7 +273,6 @@ <string name="icon_descr_more_button">Więcej</string> <string name="one_time_link">Jednorazowy link zaproszenia</string> <string name="paste_button">Wklej</string> - <string name="paste_connection_link_below_to_connect">Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem.</string> <string name="image_descr_qr_code">Kod QR</string> <string name="scan_code">Zeskanuj kod</string> <string name="scan_code_from_contacts_app">Zeskanuj kod bezpieczeństwa z aplikacji Twojego kontaktu.</string> @@ -289,7 +288,6 @@ <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Możesz też połączyć się klikając w link. Jeśli otworzy się on w przeglądarce, kliknij przycisk <b>Otwórz w aplikacji mobilnej</b>.]]></string> <string name="your_chat_profile_will_be_sent_to_your_contact">Twój profil czatu zostanie wysłany \ndo Twojego kontaktu</string> - <string name="your_profile_will_be_sent">Twój profil czatu zostanie wysłany do Twojego kontaktu</string> <string name="you_will_be_connected_when_group_host_device_is_online">Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później!</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">Zostaniesz połączony, gdy urządzenie Twojego kontaktu będzie online, proszę czekać lub sprawdzić później!</string> <string name="smp_servers_preset_add">Dodaj gotowe serwery</string> @@ -349,7 +347,8 @@ <string name="network_and_servers">Sieć i serwery</string> <string name="network_settings_title">Ustawienia sieci</string> <string name="network_use_onion_hosts_no">Nie</string> - <string name="network_use_onion_hosts_required_desc">Hosty onion będą wymagane do połączenia.</string> + <string name="network_use_onion_hosts_required_desc">Hosty onion będą wymagane do połączenia. +\nUwaga: nie będziesz mógł połączyć się z serwerami bez adresu .onion.</string> <string name="network_use_onion_hosts_required_desc_in_alert">Hosty onion będą wymagane do połączenia.</string> <string name="network_use_onion_hosts_prefer_desc">Hosty onion będą używane, gdy będą dostępne.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Hosty onion będą używane, gdy będą dostępne.</string> @@ -781,8 +780,6 @@ <string name="you_will_still_receive_calls_and_ntfs">Nadal będziesz otrzymywać połączenia i powiadomienia z wyciszonych profili, gdy są one aktywne.</string> <string name="accept_feature">Akceptuj</string> <string name="allow_your_contacts_to_send_disappearing_messages">Zezwól swoim kontaktom na wysyłanie znikających wiadomości.</string> - <string name="incognito_random_profile_from_contact_description">Losowy profil zostanie wysłany do kontaktu, od którego otrzymałeś ten link</string> - <string name="incognito_random_profile_description">Losowy profil zostanie wysłany do Twojego kontaktu</string> <string name="chat_preferences">Preferencje czatu</string> <string name="chat_preferences_contact_allows">Kontakt pozwala</string> <string name="contact_preferences">Preferencje kontaktu</string> @@ -974,10 +971,9 @@ <string name="description_via_contact_address_link_incognito">incognito poprzez link adresu kontaktowego</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Jeśli otrzymałeś link do zaproszenia SimpleX Chat, możesz go otworzyć w swojej przeglądarce:</string> <string name="import_database_question">Zaimportować bazę danych czatu\?</string> - <string name="group_unsupported_incognito_main_profile_sent">Tryb Incognito nie jest tutaj obsługiwany - główny profil zostanie wysłany do członków grupy.</string> - <string name="incognito_info_protects">Tryb incognito chroni prywatność nazwy i obrazu głównego profilu — dla każdego nowego kontaktu tworzony jest nowy losowy profil.</string> + <string name="incognito_info_protects">Tryb incognito chroni Twoją prywatność używając nowego losowego profilu dla każdego kontaktu.</string> <string name="conn_level_desc_indirect">pośrednie (%1$s)</string> - <string name="turn_off_battery_optimization"><![CDATA[Aby z niej skorzystać, należy <b>wyłączyć optymalizację baterii</b> dla SimpleX w następnym oknie dialogowym. W przeciwnym razie powiadomienia zostaną wyłączone.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Aby z niej skorzystać, należy <b>pozwolić SimpleX na działanie tle</b> w następnym oknie dialogowym. W przeciwnym razie powiadomienia zostaną wyłączone.]]></string> <string name="install_simplex_chat_for_terminal">Zainstaluj SimpleX Chat na terminal</string> <string name="invalid_migration_confirmation">Nieprawidłowe potwierdzenie migracji</string> <string name="group_invitation_item_description">zaproszenie do grupy %1$s</string> @@ -1013,7 +1009,6 @@ <string name="delete_files_and_media_desc">Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną.</string> <string name="switch_receiving_address_desc">Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</string> <string name="this_link_is_not_a_valid_connection_link">Ten link nie jest prawidłowym linkiem połączenia!</string> - <string name="incognito_info_find">Aby znaleźć profil używany do połączenia incognito, dotknij nazwę kontaktu lub grupy w górnej części czatu.</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Aby zachować prywatność, zamiast powiadomień push aplikacja posiada usługę w tle <b>SimpleX</b> - zużywa ona kilka procent baterii dziennie.]]></string> <string name="to_protect_privacy_simplex_has_ids_for_queues">Aby chronić prywatność, zamiast identyfikatorów użytkowników używanych przez wszystkie inne platformy, SimpleX ma identyfikatory dla kolejek wiadomości, oddzielne dla każdego z Twoich kontaktów.</string> <string name="to_verify_compare">Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach.</string> @@ -1322,7 +1317,7 @@ <string name="receipts_contacts_override_enabled">Wysyłanie potwierdzeń jest włączone dla %d kontaktów</string> <string name="sync_connection_force_desc">Szyfrowanie działa, a nowe uzgodnienie szyfrowania nie jest wymagane. Może to spowodować błędy w połączeniu!</string> <string name="receipts_section_description">Te ustawienia dotyczą Twojego bieżącego profilu</string> - <string name="receipts_section_description_1">Można je nadpisać w ustawieniach kontaktu</string> + <string name="receipts_section_description_1">Można je nadpisać w ustawieniach kontaktu i grupy.</string> <string name="conn_event_ratchet_sync_ok">szyfrowanie ok</string> <string name="conn_event_ratchet_sync_allowed">renegocjacja szyfrowania dozwolona</string> <string name="snd_conn_event_ratchet_sync_started">uzgadnianie szyfrowania dla %s…</string> @@ -1342,4 +1337,70 @@ <string name="no_history">Brak historii</string> <string name="sync_connection_force_confirm">Renegocjuj</string> <string name="sender_at_ts">%s o %s</string> + <string name="choose_file_title">Wybierz plik</string> + <string name="in_developing_desc">Ta funkcja nie jest jeszcze obsługiwana. Wypróbuj następną wersję.</string> + <string name="no_selected_chat">Nie wybrano czatu</string> + <string name="in_developing_title">Już wkrótce!</string> + <string name="delivery">Dostarczenie</string> + <string name="send_receipts_disabled">wyłączony</string> + <string name="receipts_groups_disable_keep_overrides">Wyłącz (zachowaj nadpisania grup)</string> + <string name="receipts_groups_title_disable">Wyłączyć potwierdzenia dla grup\?</string> + <string name="no_info_on_delivery">Brak informacji dostawy</string> + <string name="receipts_groups_title_enable">Włączyć potwierdzenia dla grup\?</string> + <string name="receipts_section_groups">Małe grupy (maks. 20)</string> + <string name="receipts_groups_override_enabled">Wysyłanie potwierdzeń jest włączone dla %d grup.</string> + <string name="receipts_groups_enable_for_all">Włącz dla wszystkich grup</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled_alert_msg">Ta grupa ma ponad %1$d członków, potwierdzenia dostawy nie są wysyłane.</string> + <string name="receipts_groups_disable_for_all">Wyłącz dla wszystkich grup</string> + <string name="receipts_groups_enable_keep_overrides">Włącz (zachowaj nadpisania grup)</string> + <string name="send_receipts_disabled_alert_title">Potwierdzenia są wyłączone</string> + <string name="receipts_groups_override_disabled">Wysyłanie potwierdzeń jest wyłączone dla %d grup.</string> + <string name="connect_via_member_address_alert_desc">Prośba o połączenie zostanie wysłana do tego członka grupy.</string> + <string name="connect_via_member_address_alert_title">Połączyć bezpośrednio\?</string> + <string name="connect_use_current_profile">Użyj obecnego profilu</string> + <string name="turn_off_battery_optimization_button">Pozwól</string> + <string name="disable_notifications_button">Wyłącz powiadomienia</string> + <string name="turn_off_system_restriction_button">Otwórz ustawienia aplikacji</string> + <string name="system_restricted_background_in_call_title">Brak rozmów w tle</string> + <string name="system_restricted_background_desc">SimpleX nie może działać w tle. Otrzymasz powiadomienia gdy aplikacja jest uruchomiona.</string> + <string name="connect__a_new_random_profile_will_be_shared">Nowy losowy profil zostanie udostępniony.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Wklej otrzymany link w pole poniżej, aby połączyć się z kontaktem…</string> + <string name="connect__your_profile_will_be_shared">Twój profil %1$s zostanie udostępniony.</string> + <string name="connect_via_link_incognito">Połącz incognito</string> + <string name="system_restricted_background_in_call_desc">Aplikacja może zostać zamknięta po 1 minucie w tle.</string> + <string name="system_restricted_background_warn"><![CDATA[Aby włączyć powiadomienia, wybierz <b>Optymalizacja baterii</b> / <b>Nieograniczony</b> w ustawieniach aplikacji.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Aby robić połączenia w tle, wybierz <b>Optymalizacja baterii</b> / <b>Nieograniczony</b> w ustawieniach aplikacji.]]></string> + <string name="connect_use_new_incognito_profile">Użyj nowego profilu incognito</string> + <string name="privacy_show_last_messages">Pokaż ostatnie wiadomości</string> + <string name="privacy_message_draft">Wersja robocza wiadomości</string> + <string name="rcv_group_event_2_members_connected">%s i %s połączeni</string> + <string name="rcv_group_event_n_members_connected">%s, %s i %d innych członków połączeni</string> + <string name="rcv_group_event_3_members_connected">%s, %s i %s połączeni</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Baza danych zostanie zaszyfrowana, a hasło zapisane w ustawieniach.</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Uwaga</b>: przekaźniki wiadomości i plików są połączone za pośrednictwem serwera proxy SOCKS. Połączenia i wysyłanie podglądów linków korzystają z połączenia bezpośredniego.]]></string> + <string name="encrypt_local_files">Zaszyfruj lokalne pliki</string> + <string name="you_can_change_it_later">Losowe hasło jest trzymane w ustawieniach w czystym tekście. +\nMożesz zmienić je później.</string> + <string name="database_encryption_will_be_updated_in_settings">Hasło szyfrowania bazy danych zostanie zaktualizowane i zapisane w ustawieniach.</string> + <string name="remove_passphrase_from_settings">Usunąć hasło z ustawień\?</string> + <string name="use_random_passphrase">Użyj losowego hasła</string> + <string name="save_passphrase_in_settings">Zapisz hasło w ustawieniach</string> + <string name="setup_database_passphrase">Ustaw hasło bazy danych</string> + <string name="set_database_passphrase">Ustaw hasło bazy danych</string> + <string name="open_database_folder">Otwórz folder bazy danych</string> + <string name="passphrase_will_be_saved_in_settings">Hasło będzie trzymane w ustawieniach jako czysty tekst po tym jak je zmienisz lub zrestartujesz aplikację.</string> + <string name="settings_is_storing_in_clear_text">Hasło jest trzymane w ustawieniach w czystym tekście.</string> + <string name="v5_3_encrypt_local_files">Szyfruj przechowywane pliki i media</string> + <string name="v5_3_new_desktop_app">Nowa aplikacja desktopowa!</string> + <string name="v5_3_new_interface_languages">6 nowych języków interfejsu</string> + <string name="v5_3_encrypt_local_files_descr">Aplikacja szyfruje nowe lokalne pliki (bez filmów).</string> + <string name="v5_3_discover_join_groups">Odkrywaj i dołączaj do grup</string> + <string name="v5_3_simpler_incognito_mode">Uproszczony tryb incognito</string> + <string name="v5_3_new_interface_languages_descr">Arabski, bułgarski, fiński, hebrajski, tajski i ukraiński - dzięki użytkownikom i Weblate.</string> + <string name="v5_3_new_desktop_app_descr">Utwórz nowy profil w aplikacji desktopowej. 💻</string> + <string name="v5_3_simpler_incognito_mode_descr">Przełącz incognito przy połączeniu.</string> + <string name="v5_3_discover_join_groups_descr">- połącz się z usługą katalogową (BETA)! +\n- potwierdzenia dostaw (do 20 członków). +\n- szybszy i stabilniejszy.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index 0f3b415469..07836dc98a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -12,7 +12,6 @@ <string name="snd_conn_event_switch_queue_phase_changing">mudando endereço…</string> <string name="change_role">Mudar função</string> <string name="change_verb">Mudar</string> - <string name="incognito_random_profile_from_contact_description">Um perfil aleatório será enviado para o contato do qual você recebeu este link</string> <string name="both_you_and_your_contacts_can_delete">Você e seu contato podem excluir mensagens enviadas de forma irreversível.</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Pode ser desativado nas configurações</b> – as notificações ainda serão exibidas enquanto o aplicativo estiver em execução.]]></string> <string name="both_you_and_your_contact_can_send_voice">Você e seu contato podem enviar mensagens de voz.</string> @@ -63,7 +62,6 @@ <string name="chat_archive_section">ARQUIVO DE CHAT</string> <string name="chat_is_stopped_indication">O chat está parado</string> <string name="clear_contacts_selection_button">Limpar</string> - <string name="incognito_random_profile_description">Um perfil aleatório será enviado para o seu contato</string> <string name="chat_preferences">Preferências de chat</string> <string name="network_session_mode_user">perfil de chat</string> <string name="icon_descr_audio_off">Áudio desligado</string> @@ -399,7 +397,6 @@ <string name="image_descr">Imagem</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Se você recebeu o link de convite SimpleX Chat, você pode abri-lo em seu navegador:</string> <string name="image_saved">Imagem salva na galeria</string> - <string name="group_unsupported_incognito_main_profile_sent">O modo de navegação anônima não é suportado aqui - seu perfil principal será enviado aos membros do grupo</string> <string name="description_via_group_link_incognito">anônimo via link do grupo</string> <string name="incoming_video_call">Chamada de vídeo recebida</string> <string name="turn_off_battery_optimization"><![CDATA[Para usá-lo, por favor <b>desative a otimização da bateria</b> para SimpleX na próxima caixa de diálogo. Caso contrário, as notificações serão desativadas.]]></string> @@ -417,7 +414,7 @@ <string name="initial_member_role">Função inicial</string> <string name="snd_group_event_group_profile_updated">perfil do grupo atualizado</string> <string name="group_member_status_group_deleted">Grupo excluído</string> - <string name="incognito_info_protects">O modo de navegação anônima protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato, um novo perfil aleatório é criado.</string> + <string name="incognito_info_protects">O modo Incognito protege sua privacidade usando um novo perfil aleatório para cada contato.</string> <string name="group_members_can_delete">Os membros do grupo podem excluir mensagens enviadas de forma irreversível.</string> <string name="ttl_w">%dsemana</string> <string name="v4_3_improved_server_configuration">Configuração de servidor aprimorada</string> @@ -460,7 +457,6 @@ <string name="feature_offered_item">ofereceu %s</string> <string name="icon_descr_address">Endereço SimpleX</string> <string name="feature_offered_item_with_param">ofereceu %s: %2s</string> - <string name="paste_connection_link_below_to_connect">Cole o link que você recebeu na caixa abaixo para conectar com o seu contato.</string> <string name="new_in_version">Novo em %s</string> <string name="send_us_an_email">Envie-nos um e-mail</string> <string name="smp_servers_scan_qr">Escanear QR code do servidor</string> @@ -473,7 +469,7 @@ <string name="simplex_service_notification_text">Recebendo mensagens…</string> <string name="large_file">Aruivo grande!</string> <string name="mark_read">Marcado como lido</string> - <string name="you_invited_your_contact">Você convidou seu contato</string> + <string name="you_invited_a_contact">Você convidou seu contato</string> <string name="invalid_QR_code">Código QR inválido</string> <string name="icon_descr_more_button">Mais</string> <string name="you_will_be_connected_when_group_host_device_is_online">Você será conectado ao grupo quando o dispositivo do host do grupo estiver online, por favor aguarde ou verifique mais tarde!</string> @@ -547,7 +543,6 @@ <string name="update_network_settings_question">Atualizar configurações de conexão\?</string> <string name="network_option_ping_count">contagem de PING</string> <string name="users_delete_with_connections">Conexões de servidor e perfil</string> - <string name="incognito_info_find">Para encontrar o perfil utilizado para uma conexão anônima, toque no contato ou no nome do grupo no topo do chat.</string> <string name="your_preferences">Suas preferências</string> <string name="set_group_preferences">Definir preferências de grupo</string> <string name="only_you_can_delete_messages">Somente você pode excluir irreversivelmente as mensagens (seu contato pode marcá-las para exclusão).</string> @@ -574,7 +569,6 @@ <string name="icon_descr_sent_msg_status_sent">enviado</string> <string name="icon_descr_sent_msg_status_send_failed">o envio falhou</string> <string name="your_chats">Chats</string> - <string name="your_profile_will_be_sent">Seu perfil de chat será enviado para seu contato</string> <string name="paste_button">Colar</string> <string name="one_time_link">Link de convite de uso único</string> <string name="chat_with_the_founder">Enviar perguntas e idéias</string> @@ -637,7 +631,7 @@ <string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string> <string name="notifications">Notificações</string> <string name="text_field_set_contact_placeholder">Definir nome do contato…</string> - <string name="switch_receiving_address_desc">Esse recurso é experimental! Ele só funcionará se o outro cliente tiver a versão 4.2 instalada. Você deve ver a mensagem na conversa assim que a alteração de endereço for concluída – verifique se você ainda pode receber mensagens desse contato (ou membro do grupo).</string> + <string name="switch_receiving_address_desc">O endereço de recebimento será alterado para um servidor diferente. A mudança de endereço terminará após o remetente entrar on-line.</string> <string name="send_verb">Enviar</string> <string name="reset_verb">Redefinir</string> <string name="live_message">Mensagem ao vivo!</string> @@ -1333,4 +1327,28 @@ <string name="enable_receipts_all">Ativar</string> <string name="sending_delivery_receipts_will_be_enabled">Enviar confirmações de entrega serão ativadas para todos os contatos.</string> <string name="error_enabling_delivery_receipts">Ocorreu um erro ao ativar as confirmações de entrega!</string> + <string name="choose_file_title">Escolher arquivo</string> + <string name="connect_via_link_incognito">Conectar incógnito</string> + <string name="turn_off_battery_optimization_button">Permitir</string> + <string name="disable_notifications_button">Desativar notificações</string> + <string name="system_restricted_background_in_call_title">Sem chamadas de fundo</string> + <string name="turn_off_system_restriction_button">Abrir configurações do aplicativo</string> + <string name="connect__a_new_random_profile_will_be_shared">Um novo perfil aleatório será compartilhado.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Cole o link que você recebeu para se conectar com seu contato..</string> + <string name="receipts_groups_title_disable">Desativar recibos para grupos\?</string> + <string name="receipts_groups_title_enable">Ativar recibos para grupos\?</string> + <string name="receipts_groups_override_disabled">Recibos de entrega estão desativados para %d grupos</string> + <string name="receipts_groups_enable_for_all">Ativar para todos os grupos</string> + <string name="receipts_groups_enable_keep_overrides">Ativar (manter sobreposições do grupo)</string> + <string name="receipts_groups_disable_keep_overrides">Desativar (manter sobreposições do grupo)</string> + <string name="receipts_groups_disable_for_all">Desativar para todos os grupos</string> + <string name="in_developing_title">Em breve!</string> + <string name="delivery">Entrega</string> + <string name="no_info_on_delivery">Nenhuma informação de entrega</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Enviar recibos de entrega será ativado para todos os contatos em todos os perfis de chat visíveis.</string> + <string name="rcv_group_event_2_members_connected">%s e %s conectados</string> + <string name="connect_via_member_address_alert_title">Conectar diretamente\?</string> + <string name="no_selected_chat">Sem chat selecionado</string> + <string name="privacy_message_draft">Rascunho de mensagem</string> + <string name="send_receipts_disabled">desativado</string> </resources> \ No newline at end of file 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 3a1e5ba55d..3de938828f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -2,7 +2,6 @@ <resources> <string name="image_descr_qr_code">Código QR</string> <string name="paste_button">Colar</string> - <string name="paste_connection_link_below_to_connect">Cole a ligação que você recebeu na caixa abaixo para se conectar ao seu contato.</string> <string name="connect_button">Conectar</string> <string name="chat_console">Consola de conversa</string> <string name="smp_servers_test_failed">Teste ao servidor falhou!</string> @@ -188,11 +187,9 @@ <string name="passcode_changed">Código de acesso alterado!</string> <string name="paste_the_link_you_received">Colar ligação recebida</string> <string name="restore_passphrase_not_found_desc">Senha não encontrada na Keystore, por favor insira-a manualmente. Isto pode ter acontecido se você restaurou os dados da aplicação usando uma ferramenta de backup. Se não for o caso, entre em contato com os desenvolvedores.</string> - <string name="incognito_random_profile_from_contact_description">Um perfil aleatório será enviado para o contato do qual você recebeu esta ligação</string> <string name="error_smp_test_server_auth">O servidor requer autorização para criar filas, verifique a senha</string> <string name="conn_stats_section_title_servers">SERVIDORES</string> <string name="error_xftp_test_server_auth">O servidor requer autorização para fazer upload, verifique a senha</string> - <string name="incognito_random_profile_description">Um perfil aleatório será enviado para o seu contato</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Defina <i>Usar hosts .onion</i> como Não se o proxy SOCKS não o suportar.]]></string> <string name="network_use_onion_hosts">Usar hosts .onion</string> <string name="smp_servers_use_server">Usar servidor</string> @@ -390,7 +387,6 @@ <string name="auto_accept_contact">Aceitar automaticamente</string> <string name="save_and_update_group_profile">Salvar e atualizar o perfil do grupo</string> <string name="network_options_save">Salvar</string> - <string name="incognito_info_find">Para localizar o perfil usado para uma ligação anónima, toque no nome do contato ou grupo na parte superior da conversa.</string> <string name="save_passphrase_and_open_chat">Salvar senha e abrir conversa</string> <string name="save_archive">Salvar arquivo</string> <string name="join_group_incognito_button">Junte-se em modo anónimo</string> @@ -399,7 +395,6 @@ <string name="error_updating_link_for_group">Erro ao atualizar a ligação do grupo</string> <string name="save_group_profile">Salvar perfil do grupo</string> <string name="incognito">Anónimo</string> - <string name="group_unsupported_incognito_main_profile_sent">O modo anónimo não é suportado aqui - o seu perfil principal será enviado para os membros do grupo</string> <string name="incognito_info_allows">Permite ter muitas ligações anónimos sem nenhum dado partilhado entre elas em nenhum perfil de conversa.</string> <string name="incognito_info_share">Quando você partilha um perfil anónimo com alguém, esse perfil será usado para os grupos para os quais ele o convide.</string> <string name="incognito_random_profile">O seu perfil aleatório</string> 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 da755b65ef..d2b4465ec4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -90,7 +90,7 @@ <string name="service_notifications_disabled">Мгновенные уведомления выключены!</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Чтобы защитить Ваши личные данные, вместо уведомлений от сервера приложение запускает <b>фоновый сервис SimpleX</b>, который потребляет несколько процентов батареи в день.]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>Он может быть выключен через Настройки</b> – Вы продолжите получать уведомления о сообщениях пока приложение запущено.]]></string> - <string name="turn_off_battery_optimization">Для использования этой функции, пожалуйста, отключите оптимизацию батареи для SimpleX в следующем диалоге. Иначе уведомления будут выключены.</string> + <string name="turn_off_battery_optimization"><![CDATA[Для использования этой функции, <b>разрешите SimpleX выполняться в фоне</b> в следующем диалоге. Иначе уведомления будут выключены.]]></string> <string name="turning_off_service_and_periodic">Оптимизация батареи включена, поэтому сервис уведомлений выключен. Вы можете снова включить его через Настройки.</string> <string name="periodic_notifications">Периодические уведомления</string> <string name="periodic_notifications_disabled">Периодические уведомления выключены!</string> @@ -294,7 +294,7 @@ <string name="mute_chat">Без звука</string> <string name="unmute_chat">Уведомлять</string> <!-- Pending contact connection alert dialogues --> - <string name="you_invited_your_contact">Вы пригласили Ваш контакт</string> + <string name="you_invited_a_contact">Вы пригласили Ваш контакт</string> <string name="you_accepted_connection">Вы приняли приглашение соединиться</string> <string name="delete_pending_connection__question">Удалить ожидаемое соединение?</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">Контакт, которому Вы отправили эту ссылку, не сможет соединиться!</string> @@ -314,7 +314,7 @@ <string name="icon_descr_settings">Настройки</string> <string name="image_descr_qr_code">QR код</string> <string name="icon_descr_address">SimpleX адрес</string> - <string name="icon_descr_help">Помощь</string> + <string name="icon_descr_help">помощь</string> <string name="icon_descr_simplex_team">SimpleX команда</string> <string name="image_descr_simplex_logo">SimpleX логотип</string> <string name="icon_descr_email">Email</string> @@ -335,8 +335,6 @@ \nВашему контакту</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Если Вы не можете встретиться лично, Вы можете <b>сосканировать QR код во время видеозвонка</b>, или Ваш контакт может отправить Вам ссылку.]]></string> <string name="share_invitation_link">Поделиться одноразовой ссылкой</string> - <string name="paste_connection_link_below_to_connect">Чтобы соединиться, вставьте в это поле ссылку, полученную от Вашего контакта.</string> - <string name="your_profile_will_be_sent">Ваш профиль будет отправлен Вашему контакту</string> <!-- PasteToConnect.kt --> <string name="connect_button">Соединиться</string> <string name="paste_button">Вставить</string> @@ -345,14 +343,14 @@ <string name="one_time_link">Одноразовая ссылка</string> <!-- settings - SettingsView.kt --> <string name="your_settings">Настройки</string> - <string name="your_simplex_contact_address">Ваш адрес SimpleX</string> - <string name="database_passphrase_and_export">Пароль и экспорт базы</string> + <string name="your_simplex_contact_address">Ваш SimpleX адрес</string> + <string name="database_passphrase_and_export">База данных</string> <string name="about_simplex_chat">Информация о SimpleX Chat</string> <string name="how_to_use_simplex_chat">Как использовать</string> <string name="markdown_help">Форматирование сообщений</string> <string name="markdown_in_messages">Форматирование сообщений</string> - <string name="chat_with_the_founder">Отправьте вопросы и идеи</string> - <string name="send_us_an_email">Отправить email</string> + <string name="chat_with_the_founder">Вопросы и предложения</string> + <string name="send_us_an_email">Написать нам письмо</string> <string name="chat_lock">Блокировка SimpleX</string> <string name="chat_console">Консоль</string> <string name="smp_servers">SMP серверы</string> @@ -405,7 +403,8 @@ <string name="network_use_onion_hosts_required">Обязательно</string> <string name="network_use_onion_hosts_prefer_desc">Onion хосты используются, если возможно.</string> <string name="network_use_onion_hosts_no_desc">Onion хосты не используются.</string> - <string name="network_use_onion_hosts_required_desc">Подключаться только к onion хостам.</string> + <string name="network_use_onion_hosts_required_desc">Подключаться только к onion хостам. +\nОбратите внимание: Вы не сможете соединиться с серверами, у которых нет .onion адреса.</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion хосты используются, если возможно.</string> <string name="network_use_onion_hosts_no_desc_in_alert">Onion хосты не используются.</string> <string name="network_use_onion_hosts_required_desc_in_alert">Подключаться только к onion хостам.</string> @@ -823,7 +822,6 @@ <string name="group_is_decentralized">Группа полностью децентрализована — она видна только членам.</string> <string name="group_display_name_field">Имя группы:</string> <string name="group_full_name_field">Полное имя:</string> - <string name="group_unsupported_incognito_main_profile_sent">Режим Инкогнито здесь не поддерживается - Ваш основной профиль будет отправлен членам группы</string> <string name="group_main_profile_sent">Ваш профиль чата будет отправлен членам группы</string> <!-- GroupProfileView.kt --> <string name="group_profile_is_stored_on_members_devices">Профиль группы хранится на устройствах членов, а не на серверах.</string> @@ -844,12 +842,9 @@ <!-- Incognito mode --> <string name="incognito">Инкогнито</string> <string name="incognito_random_profile">Случайный профиль</string> - <string name="incognito_random_profile_description">Вашему контакту будет отправлен случайный профиль</string> - <string name="incognito_random_profile_from_contact_description">Контакту, от которого Вы получили эту ссылку, будет отправлен случайный профиль</string> - <string name="incognito_info_protects">Режим Инкогнито защищает конфиденциальность имени и изображения Вашего основного профиля — для каждого нового контакта создается новый случайный профиль.</string> + <string name="incognito_info_protects">Режим Инкогнито защищает Вашу конфиденциальность — для каждого контакта создается новый случайный профиль.</string> <string name="incognito_info_allows">Это позволяет иметь много анонимных соединений без общих данных между ними в одном профиле пользователя.</string> <string name="incognito_info_share">Когда Вы соединены с контактом инкогнито, тот же самый инкогнито профиль будет использоваться для групп с этим контактом.</string> - <string name="incognito_info_find">Чтобы найти инкогнито профиль, используемый в разговоре, нажмите на имя контакта или группы в верхней части чата.</string> <!-- Default themes --> <string name="theme_system">Системная</string> <string name="theme_light">Светлая</string> @@ -868,11 +863,11 @@ <string name="chat_preferences_always">всегда</string> <string name="chat_preferences_on">да</string> <string name="chat_preferences_off">нет</string> - <string name="chat_preferences">Предпочтения</string> + <string name="chat_preferences">Настройки чатов</string> <string name="contact_preferences">Предпочтения контакта</string> <string name="group_preferences">Предпочтения группы</string> <string name="set_group_preferences">Предпочтения группы</string> - <string name="your_preferences">Ваши предпочтения</string> + <string name="your_preferences">Настройки чатов</string> <string name="direct_messages">Прямые сообщения</string> <string name="full_deletion">Удаление для всех</string> <string name="voice_messages">Голосовые сообщения</string> @@ -964,7 +959,7 @@ <string name="allow_disappearing_messages_only_if">Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам.</string> <string name="prohibit_sending_disappearing">Запретить посылать исчезающие сообщения.</string> <string name="group_members_can_send_disappearing">Члены группы могут посылать исчезающие сообщения.</string> - <string name="whats_new">Новые функции</string> + <string name="whats_new">Что нового</string> <string name="new_in_version">Новое в %s</string> <string name="v4_2_security_assessment">Аудит безопасности</string> <string name="v4_2_security_assessment_desc">Безопасность SimpleX Chat была проверена Trail of Bits.</string> @@ -1006,7 +1001,7 @@ <string name="users_delete_data_only">Только локальные данные профиля</string> <string name="messages_section_title">Сообщения</string> <string name="smp_servers_per_user">Серверы для новых соединений Вашего текущего профиля чата</string> - <string name="your_chat_profiles">Ваши профили чата</string> + <string name="your_chat_profiles">Ваши профили</string> <string name="users_delete_all_chats_deleted">Все чаты и сообщения будут удалены - это нельзя отменить!</string> <string name="app_version_code">Сборка приложения: %s</string> <string name="app_version_name">Версия приложения: v%s</string> @@ -1055,7 +1050,7 @@ <string name="smp_save_servers_question">Сохранить серверы\?</string> <string name="should_be_at_least_one_profile">Должен быть хотя бы один профиль пользователя.</string> <string name="should_be_at_least_one_visible_profile">Должен быть хотя бы один открытый профиль пользователя.</string> - <string name="to_reveal_profile_enter_password">Чтобы показать Ваш скрытый профиль, введите пароль в поле поиска на странице Ваши профили чата.</string> + <string name="to_reveal_profile_enter_password">Чтобы показать Ваш скрытый профиль, введите пароль в поле поиска на странице Ваши профили.</string> <string name="user_unmute">Уведомлять</string> <string name="group_welcome_title">Приветственное сообщение</string> <string name="confirm_password">Подтвердить пароль</string> @@ -1088,10 +1083,10 @@ <string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string> <string name="save_welcome_message_question">Сохранить приветственное сообщение\?</string> <string name="v4_6_group_welcome_message_descr">Установить сообщение для новых членов группы!</string> - <string name="tap_to_activate_profile">Нажмите, чтобы сделать профиль активным.</string> - <string name="v4_6_chinese_spanish_interface_descr">Благодаря пользователям – добавьте переводы через Weblate!</string> + <string name="tap_to_activate_profile">Нажмите на профиль, чтобы переключиться на него.</string> + <string name="v4_6_chinese_spanish_interface_descr">Благодаря пользователям - добавьте переводы через Weblate!</string> <string name="you_will_still_receive_calls_and_ntfs">Вы все равно получите звонки и уведомления в профилях без звука, когда они активные.</string> - <string name="you_can_hide_or_mute_user_profile">Вы можете скрыть профиль или выключить уведомления - подержите, чтобы увидеть меню.</string> + <string name="you_can_hide_or_mute_user_profile">Вы можете скрыть или отключить уведомления профиля - нажмите и удерживайте профиль, чтобы открыть меню.</string> <string name="image_will_be_received_when_contact_completes_uploading">Изображение будет принято когда Ваш контакт его загрузит.</string> <string name="file_will_be_received_when_contact_completes_uploading">Файл будет принят когда Ваш контакт загрузит его.</string> <string name="database_upgrade">Обновление базы данных</string> @@ -1345,7 +1340,7 @@ <string name="item_info_no_text">нет текста</string> <string name="search_verb">Поиск</string> <string name="la_mode_off">Отключено</string> - <string name="receipts_section_description_1">Они могут быть переопределены в настройках контактов</string> + <string name="receipts_section_description_1">Они могут быть изменены в настройках контактов и групп.</string> <string name="delivery_receipts_are_disabled">Отчёты о доставке выключены!</string> <string name="snd_conn_event_ratchet_sync_ok">шифрование работает для %s</string> <string name="snd_conn_event_ratchet_sync_required">требуется новое соглашение о шифровании для %s</string> @@ -1368,7 +1363,7 @@ <string name="receipts_section_description">Установки для Вашего активного профиля</string> <string name="receipts_contacts_override_disabled">Отправка отчётов о доставке выключена для %d контактов.</string> <string name="sync_connection_force_desc">Шифрование работает, и новое соглашение не требуется. Это может привести к ошибкам соединения!</string> - <string name="v5_2_message_delivery_receipts_descr">Вторая галочка - знать, что доставлено! ✅</string> + <string name="v5_2_message_delivery_receipts_descr">Вторая галочка, когда сообщение доставлено! ✅</string> <string name="you_can_enable_delivery_receipts_later_alert">Вы можете включить их позже в настройках Конфиденциальности.</string> <string name="error_aborting_address_change">Ошибка при прекращении изменения адреса</string> <string name="abort_switch_receiving_address_confirm">Прекратить</string> @@ -1402,7 +1397,7 @@ <string name="fix_connection_not_supported_by_contact">Починка не поддерживается контактом.</string> <string name="fix_connection_not_supported_by_group_member">Починка не поддерживается членом группы.</string> <string name="renegotiate_encryption">Пересогласовать шифрование</string> - <string name="v5_2_favourites_filter">Быстро найти чаты</string> + <string name="v5_2_favourites_filter">Быстрый поиск чатов</string> <string name="v5_2_message_delivery_receipts">Отчеты о доставке сообщений!</string> <string name="v5_2_more_things">Еще несколько изменений</string> <string name="delivery_receipts_title">Отчёты о доставке!</string> @@ -1424,4 +1419,70 @@ <string name="sync_connection_force_confirm">Пересогласовать</string> <string name="sync_connection_force_question">Пересогласовать шифрование\?</string> <string name="prohibit_sending_files">Запретить слать файлы и медиа.</string> + <string name="connect_via_link_incognito">Соединиться Инкогнито</string> + <string name="turn_off_battery_optimization_button">Разрешить</string> + <string name="turn_off_system_restriction_button">Открыть настройки приложения</string> + <string name="disable_notifications_button">Выключить уведомления</string> + <string name="system_restricted_background_desc">SimpleX не может выполняться в фоне. Вы получите уведомления только когда приложение запущено.</string> + <string name="system_restricted_background_in_call_title">Звонки в фоне невозможны</string> + <string name="system_restricted_background_in_call_desc">Приложение может быть выключено после 1 минуты в фоне.</string> + <string name="no_info_on_delivery">Нет информации от доставке</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Вставьте полученную ссылку, чтобы соединиться с Вашим контактом…</string> + <string name="connect__a_new_random_profile_will_be_shared">Будет отправлен новый случайный профиль.</string> + <string name="connect__your_profile_will_be_shared">Ваш профиль %1$s будет отправлен.</string> + <string name="receipts_groups_title_disable">Выключить отчёты о доставке для групп\?</string> + <string name="receipts_section_groups">Маленькие группы (до 20)</string> + <string name="receipts_groups_title_enable">Включить отчёты о доставке для групп\?</string> + <string name="receipts_groups_disable_for_all">Выключить для всех групп</string> + <string name="delivery">Доставка</string> + <string name="no_selected_chat">Чат не выбран</string> + <string name="choose_file_title">Выберите файл</string> + <string name="receipts_groups_disable_keep_overrides">Выключить (кроме исключений)</string> + <string name="receipts_groups_enable_for_all">Включить для всех групп</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="send_receipts_disabled_alert_msg">В этой группе более %1$d членов, отчёты о доставке не отправляются.</string> + <string name="rcv_group_event_n_members_connected">%s, %s и %d других членов соединены</string> + <string name="send_receipts_disabled">выключено</string> + <string name="in_developing_desc">Эта функция ещё не поддерживается. Проверьте в следующем релизе.</string> + <string name="connect_via_member_address_alert_title">Соединиться напрямую\?</string> + <string name="connect_via_member_address_alert_desc">Этому члену группы будет отправлен запрос на соединение.</string> + <string name="receipts_groups_override_enabled">Отчёты о доставке включены для %d групп</string> + <string name="privacy_show_last_messages">Показывать последние сообщения</string> + <string name="in_developing_title">Скоро!</string> + <string name="receipts_groups_enable_keep_overrides">Включить (кроме исключений)</string> + <string name="rcv_group_event_3_members_connected">%s, %s и %s соединены</string> + <string name="send_receipts_disabled_alert_title">Отчёты о доставке выключены</string> + <string name="receipts_groups_override_disabled">Отчёты о доставке выключены для %d групп</string> + <string name="rcv_group_event_2_members_connected">%s и %s соединены</string> + <string name="privacy_message_draft">Черновик сообщения</string> + <string name="system_restricted_background_warn"><![CDATA[Чтобы включить нотификации, выберите <b>Расход батареи приложением</b> / <b>Без ограничений</b> в настройках приложения.]]></string> + <string name="connect_use_current_profile">Использовать активный профиль</string> + <string name="connect_use_new_incognito_profile">Использовать новый Инкогнито профиль</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Чтобы совершать звонки в фоне, выберите <b>Расход батареи приложением</b> / <b>Без ограничений</b> в настройках приложения.]]></string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">База данных будет зашифрована, и пароль сохранен в настройках.</string> + <string name="v5_3_encrypt_local_files">Шифруйте сохраненные файлы и медиа</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>Обратите внимание</b>: соединение с серверами файлов и сообщений устанавливаются через SOCKS прокси. Звонки и картинки ссылок используют прямое соединение.]]></string> + <string name="encrypt_local_files">Шифровать локальные файлы</string> + <string name="v5_3_new_desktop_app">Приложение для компьютера!</string> + <string name="v5_3_new_interface_languages">6 новых языков интерфейса</string> + <string name="v5_3_encrypt_local_files_descr">Приложение шифрует новые локальные файлы (кроме видео).</string> + <string name="you_can_change_it_later">Случайный пароль хранится в настройках как открытый текст. +\nВы можете изменить его позже.</string> + <string name="v5_3_discover_join_groups">Найдите и вступите в группы</string> + <string name="database_encryption_will_be_updated_in_settings">Пароль шифрования базы данных будет обновлён и сохранён в настройках.</string> + <string name="remove_passphrase_from_settings">Удалить пароль из настроек\?</string> + <string name="use_random_passphrase">Использовать случайный пароль</string> + <string name="save_passphrase_in_settings">Сохранить пароль в настройках</string> + <string name="v5_3_simpler_incognito_mode">Упрощенный режим Инкогнито</string> + <string name="setup_database_passphrase">Установить пароль базы данных</string> + <string name="set_database_passphrase">Установить пароль базы данных</string> + <string name="open_database_folder">Открыть директорию базы данных</string> + <string name="v5_3_new_interface_languages_descr">Арабский, болгарский, финский, иврит, тайский и украинский - благодаря пользователям и Weblate.</string> + <string name="v5_3_new_desktop_app_descr">Создайте новый профиль в приложении для компьютера. 💻</string> + <string name="passphrase_will_be_saved_in_settings">Пароль будет сохранён в настройках как простой текст после того, как вы его измените или перезапустите приложение.</string> + <string name="v5_3_simpler_incognito_mode_descr">Установите режим Инкогнито при соединении.</string> + <string name="v5_3_discover_join_groups_descr">- соединиться с каталогом групп (BETA)! +\n- отчеты о доставке (до 20 членов). +\n- быстрее и стабильнее.</string> + <string name="settings_is_storing_in_clear_text">Пароль хранится в настройках, как открытый текст.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index f2f7fd70c7..ad9cff0cc5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -31,10 +31,8 @@ <string name="audio_call_no_encryption">การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ)</string> <string name="notifications_mode_off_desc">แอปสามารถรับการแจ้งเตือนได้เฉพาะเมื่อกำลังทำงานอยู่เท่านั้น โดยจะไม่มีการเริ่มบริการพื้นหลัง</string> <string name="appearance_settings">รูปร่างลักษณะ</string> - <string name="incognito_random_profile_from_contact_description">โปรไฟล์แบบสุ่มจะถูกส่งไปยังผู้ติดต่อที่คุณได้รับลิงก์นี้จาก</string> - <string name="incognito_random_profile_description">โปรไฟล์แบบสุ่มจะถูกส่งไปยังผู้ติดต่อของคุณ</string> <string name="network_session_mode_user_description"><![CDATA[การเชื่อมต่อ TCP (และข้อมูลรับรอง SOCKS) แบบแยกต่างหาก จะถูกใช้ <b> สําหรับแต่ละโปรไฟล์แชทที่คุณมีในแอป </b>]]></string> - <string name="network_session_mode_entity_description">การเชื่อมต่อ TCP (และข้อมูลรับรอง SOCKS) แบบแยกต่างหาก จะถูกใช้ <b> สําหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่มแต่ละคน </b>\n<b>โปรดทราบ </b>: หากคุณมีการเชื่อมต่อจํานวนมากแบตเตอรี่และปริมาณการใช้การจราจรของคุณอาจสูงขึ้นอย่างมากและการเชื่อมต่อบางอย่างอาจล้มเหลว</string> + <string name="network_session_mode_entity_description"><![CDATA[การเชื่อมต่อ TCP (และข้อมูลรับรอง SOCKS) แบบแยกต่างหาก จะถูกใช้ <b> สําหรับผู้ติดต่อแต่ละคนและสมาชิกกลุ่มแต่ละคน </b>\n<b>โปรดทราบ </b>: หากคุณมีการเชื่อมต่อจํานวนมากแบตเตอรี่และปริมาณการใช้การจราจรของคุณอาจสูงขึ้นอย่างมากและการเชื่อมต่อบางอย่างอาจล้มเหลว]]></string> <string name="attach">แนบ</string> <string name="v4_6_audio_video_calls">การโทรด้วยเสียงและวิดีโอ</string> <string name="auto_accept_contact">ยอมรับอัตโนมัติ</string> @@ -111,7 +109,7 @@ <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> ดีที่สุดสําหรับแบตเตอรี่ </b> คุณจะได้รับการแจ้งเตือนเฉพาะเมื่อแอปกําลังทํางานอยู่ (ไม่มีบริการพื้นหลัง)]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> ดีสำหรับแบตเตอรี่ </b> บริการพื้นหลังจะตรวจสอบข้อความทุกๆ 10 นาที คุณอาจไม่ได้รับสายหรือข้อความด่วน]]></string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> สามารถปิดใช้งานผ่านการตั้งค่า </b> - การแจ้งเตือนจะยังคงปรากฏขึ้นในขณะที่แอปกําลังทํางานอยู่]]></string> - <string name="both_you_and_your_contact_can_add_message_reactions">ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้</string> + <string name="both_you_and_your_contact_can_add_message_reactions">ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาต่อข้อความได้</string> <string name="both_you_and_your_contacts_can_delete">ทั้งคุณและผู้ติดต่อของคุณสามารถลบข้อความที่ส่งแล้วอย่างถาวรได้</string> <string name="use_camera_button">กล้อง</string> <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b> เพิ่มผู้ติดต่อใหม่ </b>: เพื่อสร้างรหัส QR แบบใช้ครั้งเดียวสําหรับผู้ติดต่อของคุณ]]></string> @@ -131,7 +129,7 @@ <string name="both_you_and_your_contact_can_make_calls">ทั้งคุณและผู้ติดต่อของคุณสามารถโทรออกได้</string> <string name="v5_1_better_messages">ข้อความที่ดีขึ้น</string> <string name="cannot_receive_file">ไม่สามารถรับไฟล์ได้</string> - <string name="database_initialization_error_title">สามารถเริ่มเตรียมการใช้งานฐานข้อมูล ได้</string> + <string name="database_initialization_error_title">ไม่สามารถเริ่มต้นการใช้งานฐานข้อมูล ได้</string> <string name="la_change_app_passcode">เปลี่ยนรหัสผ่าน</string> <string name="icon_descr_cancel_file_preview">ยกเลิกการแสดงตัวอย่างไฟล์</string> <string name="icon_descr_cancel_image_preview">ยกเลิกการแสดงตัวอย่างรูปภาพ</string> @@ -139,15 +137,15 @@ <string name="icon_descr_cancel_link_preview">ยกเลิกการแสดงตัวอย่างลิงก์</string> <string name="change_self_destruct_mode">เปลี่ยนโหมดทําลายตัวเอง</string> <string name="cannot_access_keychain">ไม่สามารถเข้าถึง Keystore เพื่อบันทึกรหัสผ่านฐานข้อมูล</string> - <string name="rcv_group_event_changed_member_role">เปลี่ยนบทบาทของ %s เป็น %s</string> - <string name="rcv_conn_event_switch_queue_phase_completed">เปลี่ยนที่อยู่สําหรับคุณ</string> + <string name="rcv_group_event_changed_member_role">เปลี่ยนบทบาทของ %s เป็น %s แล้ว</string> + <string name="rcv_conn_event_switch_queue_phase_completed">เปลี่ยนที่อยู่สําหรับคุณแล้ว</string> <string name="invite_prohibited">ไม่สามารถเชิญผู้ติดต่อได้!</string> <string name="change_role">เปลี่ยนบทบาท</string> <string name="change_member_role_question">เปลี่ยนบทบาทกลุ่ม\?</string> <string name="icon_descr_cancel_live_message">ยกเลิกข้อความสด</string> <string name="feature_cancelled_item">ยกเลิกเรียบร้อยแล้ว %s</string> <string name="cant_delete_user_profile">ไม่สามารถลบโปรไฟล์ผู้ใช้ได้!</string> - <string name="alert_title_cant_invite_contacts">ไม่สามารถเชิญผู้ติดต่อทั้งหลายได้!</string> + <string name="alert_title_cant_invite_contacts">ไม่สามารถเชิญผู้ติดต่อได้!</string> <string name="change_verb">เปลี่ยน</string> <string name="change_database_passphrase_question">เปลี่ยนรหัสผ่านฐานข้อมูล\?</string> <string name="rcv_group_event_changed_your_role">เปลี่ยนบทบาทของคุณเป็น %s เรียบร้อยแล้ว</string> @@ -158,10 +156,10 @@ <string name="server_connected">เชื่อมต่อสำเร็จ</string> <string name="contact_connection_pending">กำลังเชื่อมต่อ…</string> <string name="group_member_status_accepted">กำลังเชื่อมต่อ (ยอมรับแล้ว)</string> - <string name="icon_descr_call_connecting">การเชื่อมต่อการโทร</string> - <string name="group_member_status_introduced">กำลังการเชื่อมต่อ (แนะนําแล้ว)</string> + <string name="icon_descr_call_connecting">กำลังเชื่อมต่อการโทร</string> + <string name="group_member_status_introduced">กำลังเชื่อมต่อ (แนะนํา)</string> <string name="connection_request_sent">ส่งคําขอเชื่อมต่อแล้ว!</string> - <string name="connection_error">การเชื่อมต่อล้มเหลว</string> + <string name="connection_error">การเชื่อมต่อผิดพลาด</string> <string name="connection_timeout">หมดเวลาการเชื่อมต่อ</string> <string name="connect_via_group_link">เชื่อมต่อผ่านลิงค์กลุ่ม\?</string> <string name="delete_contact_all_messages_deleted_cannot_undo_warning">ผู้ติดต่อและข้อความทั้งหมดจะถูกลบ - ไม่สามารถยกเลิกได้!</string> @@ -178,13 +176,13 @@ <string name="display_name_connection_established">สร้างการเชื่อมต่อแล้ว</string> <string name="connection_local_display_name">การเชื่อมต่อ %1$d</string> <string name="contact_already_exists">ผู้ติดต่อรายนี้มีอยู่แล้ว</string> - <string name="connection_error_auth">การเชื่อมต่อล้มเหลว (AUTH)</string> + <string name="connection_error_auth">การเชื่อมต่อผิดพลาด (AUTH)</string> <string name="smp_server_test_compare_file">เปรียบเทียบไฟล์</string> <string name="smp_server_test_connect">เชื่อมต่อ</string> <string name="smp_server_test_create_file">สร้างไฟล์</string> <string name="smp_server_test_create_queue">สร้างคิว</string> <string name="notifications_mode_periodic_desc">ตรวจสอบข้อความใหม่ทุกๆ 10 นาที นานถึง 1 นาที</string> - <string name="notification_preview_somebody">ซ่อนผู้ติดต่อ:</string> + <string name="notification_preview_somebody">ผู้ติดต่อถูกซ่อน:</string> <string name="notification_preview_mode_contact">ชื่อผู้ติดต่อ</string> <string name="notification_contact_connected">เชื่อมต่อสำเร็จ</string> <string name="la_current_app_passcode">รหัสผ่านปัจจุบัน</string> @@ -218,7 +216,7 @@ <string name="core_version">เวอร์ชันหลัก: v%s</string> <string name="customize_theme_title">ปรับแต่งธีม</string> <string name="create_address">สร้างที่อยู่</string> - <string name="create_address_and_let_people_connect">สร้างที่อยู่เพื่อให้ผู้คนติดต่อกับคุณ</string> + <string name="create_address_and_let_people_connect">สร้างที่อยู่เพื่อให้ผู้อื่นเชื่อมต่อกับคุณ</string> <string name="create_simplex_address">สร้างที่อยู่ SimpleX</string> <string name="continue_to_next_step">ดำเนินการต่อ</string> <string name="create_profile_button">สร้าง</string> @@ -234,19 +232,19 @@ <string name="change_self_destruct_passcode">เปลี่ยนรหัสผ่านแบบทำลายตัวเอง</string> <string name="settings_section_title_chats">แชท</string> <string name="chat_database_section">ฐานข้อมูลแชท</string> - <string name="chat_is_running">แชทกําลังทํางานอยู่</string> + <string name="chat_is_running">แชทกำลังทำงานอยู่</string> <string name="chat_is_stopped">การแชทหยุดทํางานแล้ว</string> <string name="chat_database_imported">นำฐานข้อมูลแชทเข้าแล้ว</string> <string name="current_passphrase">รหัสผ่านปัจจุบัน…</string> - <string name="database_encrypted">encrypt ฐานข้อมูลอย่างปลอดภัยแล้ว!</string> + <string name="database_encrypted">ฐานข้อมูลถูก encrypt แล้ว!</string> <string name="confirm_new_passphrase">ยืนยันรหัสผ่านใหม่…</string> - <string name="database_passphrase_will_be_updated">รหัส encryption ของฐานข้อมูลจะได้รับการอัปเดต</string> - <string name="database_encryption_will_be_updated">รหัส encryption ของฐานข้อมูลจะได้รับการอัปเดตและจัดเก็บไว้ใน Keystore</string> + <string name="database_passphrase_will_be_updated">รหัสผ่าน encryption ของฐานข้อมูลจะได้รับการอัปเดต</string> + <string name="database_encryption_will_be_updated">รหัสผ่าน encryption ของฐานข้อมูลจะได้รับการอัปเดตและจัดเก็บไว้ใน Keystore</string> <string name="database_error">ความผิดพลาดในฐานข้อมูล</string> <string name="confirm_database_upgrades">ยืนยันการอัพเกรดฐานข้อมูล</string> <string name="database_downgrade">ดาวน์เกรดฐานข้อมูล</string> - <string name="chat_archive_header">ที่เก็บแชทเก่า</string> - <string name="chat_archive_section">ที่เก็บแชทเก่า</string> + <string name="chat_archive_header">ที่เก็บแชทถาวร</string> + <string name="chat_archive_section">ที่เก็บแชทถาวร</string> <string name="chat_is_stopped_indication">การแชทหยุดทํางานแล้ว</string> <string name="rcv_group_event_member_connected">เชื่อมต่อสำเร็จ</string> <string name="rcv_conn_event_switch_queue_phase_changing">กำลังเปลี่ยนที่อยู่…</string> @@ -269,26 +267,26 @@ <string name="theme_dark">มืด</string> <string name="dark_theme">ธีมมืด</string> <string name="chat_preferences_contact_allows">ผู้ติดต่ออนุญาต</string> - <string name="chat_preferences">การตั้งค่าการแชท</string> - <string name="contact_preferences">การตั้งค่าผู้ติดต่อ</string> + <string name="chat_preferences">ค่ากําหนดในการแชท</string> + <string name="contact_preferences">การกําหนดลักษณะการติดต่อ</string> <string name="contacts_can_mark_messages_for_deletion">ผู้ติดต่อสามารถทําเครื่องหมายข้อความเพื่อลบได้ คุณจะสามารถดูได้</string> <string name="v4_6_chinese_spanish_interface">อินเทอร์เฟซภาษาจีนและสเปน</string> <string name="v5_1_custom_themes_descr">ปรับแต่งและแชร์ธีมสี</string> <string name="custom_time_picker_custom">กำหนดเอง</string> <string name="file_will_be_received_when_contact_is_online">จะได้รับไฟล์เมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</string> - <string name="alert_message_group_invitation_expired">คำเชิญเข้าร่วมกลุ่มใช้ไม่ได้อีกต่อไป คำเชิญถูกลบโดยผู้ส่ง</string> + <string name="alert_message_group_invitation_expired">คำเชิญเข้าร่วมกลุ่มใช้ไม่ถูกต้องอีกต่อไป คำเชิญถูกลบโดยผู้ส่ง</string> <string name="group_members_can_delete">สมาชิกกลุ่มสามารถลบข้อความที่ส่งแล้วอย่างถาวร</string> <string name="group_profile_is_stored_on_members_devices">โปรไฟล์กลุ่มถูกจัดเก็บไว้ในอุปกรณ์ของสมาชิก ไม่ใช่บนเซิร์ฟเวอร์</string> <string name="delete_group_for_self_cannot_undo_warning">กลุ่มจะถูกลบสำหรับคุณ - ไม่สามารถยกเลิกได้!</string> <string name="set_password_to_export_desc">ฐานข้อมูลถูก encrypt โดยใช้รหัสผ่านแบบสุ่ม โปรดเปลี่ยนก่อนส่งออก</string> - <string name="decryption_error">ข้อผิดพลาดในการถอดรหัส</string> + <string name="decryption_error">ข้อผิดพลาดในการ decrypt</string> <string name="rcv_group_event_group_deleted">กลุ่มที่ถูกลบ</string> - <string name="delete_group_question">ลบกลุ่มไหม\?</string> + <string name="delete_group_question">ลบกลุ่ม\?</string> <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">การตรวจสอบอุปกรณ์ไม่ได้ถูกเปิดใช้งาน คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่าได้ เมื่อคุณเปิดใช้งานการตรวจสอบอุปกรณ์แล้ว</string> <string name="enter_welcome_message_optional">ใส่ข้อความต้อนรับ… (ไม่บังคับ)</string> <string name="error_loading_smp_servers">เกิดข้อผิดพลาดในการโหลดเซิร์ฟเวอร์ SMP</string> <string name="error_loading_xftp_servers">เกิดข้อผิดพลาดในการโหลดเซิร์ฟเวอร์ XFTP</string> - <string name="server_error">ผิดพลาด</string> + <string name="server_error">ข้อผิดพลาด</string> <string name="deleted_description">ลบแล้ว</string> <string name="simplex_link_mode_full">ลิงค์เต็ม</string> <string name="simplex_link_mode_description">คำอธิบาย</string> @@ -345,9 +343,9 @@ <string name="button_delete_contact">ลบผู้ติดต่อ</string> <string name="delete_contact_question">ลบผู้ติดต่อ\?</string> <string name="error_saving_file">เกิดข้อผิดพลาดในการบันทึกไฟล์</string> - <string name="icon_descr_server_status_disconnected">ตัดการเชื่อมต่อ</string> - <string name="icon_descr_server_status_error">ผิดพลาด</string> - <string name="disappearing_message">ข้อความที่จะหายไปหลังปิดแชท (disappearing message)</string> + <string name="icon_descr_server_status_disconnected">ตัดการเชื่อมต่อแล้ว</string> + <string name="icon_descr_server_status_error">ข้อผิดพลาด</string> + <string name="disappearing_message">ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message)</string> <string name="choose_file">ไฟล์</string> <string name="from_gallery_button">จากแกลเลอรี</string> <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 เดสก์ท็อป: สแกนรหัส QR ที่แสดงอยู่บนแอปผ่าน <b> สแกนรหัส QR </b>]]></string> @@ -379,7 +377,7 @@ <string name="edit_image">แก้ไขภาพ</string> <string name="hidden_profile_password">รหัสผ่านโปรไฟล์ที่ซ่อนอยู่</string> <string name="hide_profile">ซ่อนโปรไฟล์</string> - <string name="error_saving_user_password">เกิดข้อผิดพลาดในการบันทึกรหัสผ่านของผู้ใช้</string> + <string name="error_saving_user_password">เกิดข้อผิดพลาดในการบันทึกรหัสผ่านผู้ใช้</string> <string name="full_name_optional__prompt">ชื่อเต็ม (ไม่บังคับ)</string> <string name="display_name">ชื่อที่แสดง</string> <string name="display_name_cannot_contain_whitespace">ชื่อที่แสดงต้องไม่มีช่องว่าง</string> @@ -387,9 +385,9 @@ <string name="callstate_ended">สิ้นสุดลงแล้ว</string> <string name="how_it_works">มันทำงานอย่างไร</string> <string name="how_simplex_works">วิธีการ SimpleX ทํางานอย่างไร</string> - <string name="decentralized">กระจายอำนาจ</string> - <string name="encrypted_audio_call">การโทรเสียงแบบ encrypt จากต้นจนจบ</string> - <string name="encrypted_video_call">การโทรวิดีแบบ encrypt จากต้นจนจบ</string> + <string name="decentralized">กระจายอำนาจแล้ว</string> + <string name="encrypted_audio_call">การโทรเสียงแบบ encrypted จากต้นจนจบ</string> + <string name="encrypted_video_call">การโทรวิดีแบบ encrypted จากต้นจนจบ</string> <string name="no_call_on_lock_screen">ปิดการใช้งาน</string> <string name="status_e2e_encrypted">encrypted จากต้นจนจบ</string> <string name="allow_accepting_calls_from_lock_screen">เปิดใช้งานการโทรจากหน้าจอล็อคผ่านการตั้งค่า</string> @@ -417,10 +415,10 @@ <string name="delete_files_and_media_all">ลบไฟล์ทั้งหมด</string> <string name="delete_files_and_media_question">ลบไฟล์และสื่อ\?</string> <string name="delete_files_and_media_for_all_users">ลบไฟล์สําหรับโปรไฟล์แชททั้งหมด</string> - <string name="total_files_count_and_size">ไฟล์ %d ที่มีขนาดรวมเป็น %s</string> - <string name="delete_messages_after">ลบข้อความทีหลัง</string> + <string name="total_files_count_and_size">%d ไฟล์ที่มีขนาดรวมเป็น %s</string> + <string name="delete_messages_after">ลบข้อความหลังจาก</string> <string name="delete_messages">ลบข้อความ</string> - <string name="enable_automatic_deletion_question">เปิดใช้งานการลบข้อความอัตโนมัติไหม\?</string> + <string name="enable_automatic_deletion_question">เปิดใช้งานการลบข้อความอัตโนมัติ\?</string> <string name="error_changing_message_deletion">เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า</string> <string name="encrypt_database">Encrypt</string> <string name="error_encrypting_database">เกิดข้อผิดพลาดในการ encrypt ฐานข้อมูล</string> @@ -436,7 +434,7 @@ <string name="enter_correct_passphrase">ใส่รหัสผ่านที่ถูกต้อง</string> <string name="enter_passphrase">ใส่รหัสผ่าน</string> <string name="database_upgrade">อัพเกรดฐานข้อมูล</string> - <string name="mtr_error_no_down_migration">เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีลดเวอร์ชันสำหรับ: %s</string> + <string name="mtr_error_no_down_migration">เวอร์ชันฐานข้อมูลใหม่กว่าแอป แต่ไม่มีการย้ายข้อมูลลงสำหรับ: %s</string> <string name="mtr_error_different">การย้ายข้อมูลที่แตกต่างกันในแอป/ฐานข้อมูล: %s / %s</string> <string name="downgrade_and_open_chat">ปรับลดรุ่นและเปิดแชท</string> <string name="delete_archive">ลบที่เก็บถาวร</string> @@ -448,7 +446,7 @@ <string name="group_member_status_group_deleted">ลบกลุ่มแล้ว</string> <string name="icon_descr_expand_role">ขยายการเลือกบทบาท</string> <string name="delete_group_for_all_members_cannot_undo_warning">กลุ่มจะถูกลบสำหรับสมาชิกทั้งหมด - ไม่สามารถยกเลิกได้!</string> - <string name="num_contacts_selected">เลือกผู้ติดต่อ %d แล้ว</string> + <string name="num_contacts_selected">%d เลือกผู้ติดต่อแล้ว</string> <string name="button_delete_group">ลบกลุ่ม</string> <string name="group_link">ลิงค์กลุ่ม</string> <string name="delete_link_question">ลบลิงค์ ไหม\?</string> @@ -459,7 +457,7 @@ <string name="error_updating_link_for_group">เกิดข้อผิดพลาดในการอัปเดตลิงก์กลุ่ม</string> <string name="section_title_for_console">สำหรับคอนโซล</string> <string name="info_row_deleted_at">ลบที่</string> - <string name="share_text_deleted_at">ลบเมื่อ: %s</string> + <string name="share_text_deleted_at">ลบที่: %s</string> <string name="info_row_disappears_at">หายไปที่</string> <string name="share_text_disappears_at">หายไปที่: %s</string> <string name="info_row_group">กลุ่ม</string> @@ -480,19 +478,19 @@ <string name="delete_profile">ลบโปรไฟล์</string> <string name="export_theme">ส่งออกธีม</string> <string name="chat_preferences_default">ค่าเริ่มต้น (%s)</string> - <string name="group_preferences">การตั้งค่ากลุ่ม</string> - <string name="direct_messages">ข้อความส่วนตัว</string> + <string name="group_preferences">ค่ากําหนดลักษณะกลุ่ม</string> + <string name="direct_messages">ข้อความโดยตรง</string> <string name="timed_messages">ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages)</string> <string name="full_deletion">ลบสำหรับทุกคน</string> - <string name="feature_enabled">เปิดใช้งาน</string> + <string name="feature_enabled">เปิดใช้งานแล้ว</string> <string name="feature_enabled_for_you">เปิดใช้งานสําหรับคุณแล้ว</string> <string name="feature_enabled_for_contact">ได้เปิดใช้งานสำหรับการติดต่อแล้ว</string> <string name="disappearing_prohibited_in_this_chat">ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) เป็นสิ่งต้องห้ามในแชทนี้</string> <string name="message_deletion_prohibited">ไม่สามารถลบข้อความแบบแก้ไขไม่ได้ในแชทนี้</string> <string name="group_members_can_send_disappearing">สมาชิกกลุ่มสามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</string> - <string name="group_members_can_send_dms">สมาชิกกลุ่มสามารถส่งข้อความส่วนตัวได้</string> + <string name="group_members_can_send_dms">สมาชิกกลุ่มสามารถส่งข้อความโดยตรงได้</string> <string name="group_members_can_send_voice">สมาชิกกลุ่มสามารถส่งข้อความเสียง</string> - <string name="direct_messages_are_prohibited_in_chat">ข้อความส่วนตัวระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</string> + <string name="direct_messages_are_prohibited_in_chat">ข้อความโดยตรงระหว่างสมาชิกเป็นสิ่งต้องห้ามในกลุ่มนี้</string> <string name="disappearing_messages_are_prohibited">ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) เป็นสิ่งต้องห้ามในกลุ่มนี้</string> <string name="group_members_can_add_message_reactions">สมาชิกกลุ่มสามารถเพิ่มการแสดงปฏิกิริยาต่อข้อความได้</string> <string name="delete_after">ลบหลังจาก</string> @@ -517,11 +515,11 @@ <string name="v4_3_improved_privacy_and_security_desc">ซ่อนหน้าจอแอพในแอพล่าสุด</string> <string name="v4_4_disappearing_messages">ข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages)</string> <string name="v4_4_french_interface">อินเทอร์เฟซภาษาฝรั่งเศส</string> - <string name="v4_5_multiple_chat_profiles_descr">ชื่ออวตารและการแยกการขนส่งที่แตกต่างกัน</string> + <string name="v4_5_multiple_chat_profiles_descr">ชื่อ อวตาร และการแยกการขนส่งที่แตกต่างกัน</string> <string name="v4_5_italian_interface">อินเทอร์เฟซภาษาอิตาลี</string> <string name="v4_6_hidden_chat_profiles">โปรไฟล์การแชทที่ซ่อนอยู่</string> <string name="v4_6_reduced_battery_usage">ลดการใช้แบตเตอรี่เพิ่มเติม</string> - <string name="v4_6_group_moderation">การดูแลกลุ่ม</string> + <string name="v4_6_group_moderation">การกลั่นกรองกลุ่ม</string> <string name="v4_6_group_welcome_message">ข้อความต้อนรับกลุ่ม</string> <string name="v5_0_large_files_support_descr">รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!</string> <string name="v5_1_message_reactions_descr">ในที่สุดเราก็มีแล้ว! 🚀</string> @@ -530,12 +528,11 @@ <string name="error_receiving_file">เกิดข้อผิดพลาดในการรับไฟล์</string> <string name="if_you_enter_passcode_data_removed">หากคุณใส่รหัสผ่านนี้เมื่อเปิดแอป ข้อมูลแอปทั้งหมดจะถูกลบอย่างถาวร!</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">หากคุณเลือกที่จะปฏิเสธ ผู้ส่งจะไม่ได้รับแจ้ง</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[หากคุณไม่สามารถพบกันในชีวิตจริงได้ คุณสามารถสแกนคิวอาร์โค้ดในวิดีโอคอล </b> หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้]]></string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">หากคุณไม่สามารถพบกันในชีวิตจริงได้ คุณสามารถสแกนคิวอาร์โค้ดในวิดีโอคอล หรือผู้ติดต่อของคุณสามารถแชร์ลิงก์เชิญได้</string> <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[หากคุณไม่สามารถพบกันในชีวิตจริงได้ <b>ให้แสดงคิวอาร์โค้ดในวิดีโอคอล</b> หรือแชร์ลิงก์]]></string> <string name="if_you_cant_meet_in_person">หากคุณไม่สามารถพบกันในชีวิตจริงได้ ให้แสดงคิวอาร์โค้ดในวิดีโอคอล หรือแชร์ลิงก์</string> <string name="network_disable_socks_info">หากคุณยืนยัน เซิร์ฟเวอร์การส่งข้อความจะสามารถเห็นที่อยู่ IP ของคุณและผู้ให้บริการของคุณ - ซึ่งคือเซิร์ฟเวอร์ใดที่คุณกำลังเชื่อมต่ออยู่</string> - <string name="if_you_enter_self_destruct_code">หากคุณใส่รหัสทำลายตัวเองขณะเปิดแอป:</string> - <string name="group_unsupported_incognito_main_profile_sent">ไม่รองรับโหมดไม่ระบุตัวตนที่นี่ - โปรไฟล์หลักของคุณจะถูกส่งไปยังสมาชิกกลุ่ม</string> + <string name="if_you_enter_self_destruct_code">หากคุณใส่รหัสผ่านทำลายตัวเองขณะเปิดแอป:</string> <string name="incognito_info_protects">โหมดไม่ระบุตัวตนปกป้องความเป็นส่วนตัวของชื่อและรูปภาพโปรไฟล์หลักของคุณ — สำหรับผู้ติดต่อใหม่แต่ละราย จะมีการสร้างโปรไฟล์ใหม่แบบสุ่ม</string> <string name="conn_level_desc_indirect">ทางอ้อม (%1$s)</string> <string name="install_simplex_chat_for_terminal">ติดตั้ง SimpleX Chat สำหรับเทอร์มินัล</string> @@ -552,10 +549,10 @@ <string name="display_name_invited_to_connect">ได้รับเชิญให้เชื่อมต่อ</string> <string name="description_via_contact_address_link_incognito">ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ</string> <string name="description_via_group_link_incognito">ไม่ระบุตัวตนผ่านลิงก์กลุ่ม</string> - <string name="description_via_one_time_link_incognito">ไม่ระบุตัวตนผ่านลิงก์แบบครั้งเดียว</string> + <string name="description_via_one_time_link_incognito">ไม่ระบุตัวตนผ่านลิงก์แบบใช้ครั้งเดียว</string> <string name="invalid_connection_link">ลิงค์เชื่อมต่อไม่ถูกต้อง</string> <string name="icon_descr_instant_notifications">การแจ้งเตือนทันที</string> - <string name="service_notifications">แจ้งเตือนทันที!</string> + <string name="service_notifications">การแจ้งเตือนทันที!</string> <string name="service_notifications_disabled">ปิดการแจ้งเตือนทันทีแล้ว!</string> <string name="turn_off_battery_optimization"><![CDATA[เพื่อให้สามารถใช้งานได้ โปรด <b>ปิดการใช้งานการเพิ่มประสิทธิภาพแบตเตอรี่</b> สำหรับ SimpleX ในกล่องโต้ตอบถัดไป มิฉะนั้น การแจ้งเตือนจะถูกปิดใช้งาน]]></string> <string name="la_immediately">โดยทันที</string> @@ -563,7 +560,7 @@ <string name="info_menu">ข้อมูล</string> <string name="group_preview_join_as">เข้าร่วมในชื่อ %s</string> <string name="image_descr">ภาพ</string> - <string name="icon_descr_image_snd_complete">ส่งรูปภาพแล้ว</string> + <string name="icon_descr_image_snd_complete">ส่งภาพแล้ว</string> <string name="image_will_be_received_when_contact_completes_uploading">จะได้รับภาพเมื่อผู้ติดต่อของคุณอัปโหลดเสร็จสิ้น</string> <string name="image_will_be_received_when_contact_is_online">จะได้รับรูปภาพเมื่อผู้ติดต่อของคุณออนไลน์ โปรดรอหรือตรวจสอบในภายหลัง!</string> <string name="image_saved">ภาพถูกบันทึกไว้ในแกลเลอรีแล้ว</string> @@ -578,10 +575,10 @@ <string name="incorrect_code">รหัสความปลอดภัยไม่ถูกต้อง!</string> <string name="smp_servers_invalid_address">ที่อยู่เซิร์ฟเวอร์ไม่ถูกต้อง!</string> <string name="invite_friends">เชิญเพื่อน ๆ</string> - <string name="email_invite_subject">มาพูดคุยกันใน SimpleX Chat</string> + <string name="email_invite_subject">มาคุยกันใน SimpleX Chat</string> <string name="italic_text">ตัวเอียง</string> - <string name="immune_to_spam_and_abuse">ป้องกันจากสแปมและการละเมิด</string> - <string name="make_private_connection">สร้างการเชื่อมต่อส่วนตัว</string> + <string name="immune_to_spam_and_abuse">มีภูมิคุ้มกันต่อสแปมและการละเมิด</string> + <string name="make_private_connection">สร้างการเชื่อมต่อแบบส่วนตัว</string> <string name="onboarding_notifications_mode_subtitle">สามารถเปลี่ยนแปลงได้ในภายหลังผ่านการตั้งค่า</string> <string name="incoming_video_call">สายวิดีโอเข้ามา</string> <string name="onboarding_notifications_mode_service">ทันที</string> @@ -597,7 +594,7 @@ <string name="import_database_question">นำเข้าฐานข้อมูลแชท\?</string> <string name="keychain_error">ข้อผิดพลาดของ Keychain</string> <string name="incompatible_database_version">เวอร์ชันฐานข้อมูลที่เข้ากันไม่ได้</string> - <string name="invalid_migration_confirmation">การยืนยันการย้ายไม่ถูกต้อง</string> + <string name="invalid_migration_confirmation">การยืนยันการย้ายข้อมูลไม่ถูกต้อง</string> <string name="group_invitation_item_description">คำเชิญเข้าร่วมกลุ่ม %1$s</string> <string name="join_group_button">เข้าร่วม</string> <string name="join_group_question">เข้าร่วมกลุ่ม\?</string> @@ -608,14 +605,14 @@ <string name="icon_descr_add_members">เชิญสมาชิก</string> <string name="rcv_group_event_member_added">เชิญแล้ว %1$s</string> <string name="rcv_group_event_member_left">ออกแล้ว</string> - <string name="rcv_group_event_invited_via_your_group_link">เชิญผ่านลิงค์กลุ่มของคุณ</string> - <string name="group_member_status_invited">เชิญ</string> + <string name="rcv_group_event_invited_via_your_group_link">ถูกเชิญผ่านลิงค์กลุ่มของคุณ</string> + <string name="group_member_status_invited">เชิญแล้ว</string> <string name="group_member_status_left">ออกแล้ว</string> <string name="initial_member_role">บทบาทเริ่มต้น</string> <string name="invite_to_group_button">เชิญเข้าร่วมกลุ่ม</string> <string name="button_add_members">เชิญสมาชิก</string> <string name="button_leave_group">ออกจากกลุ่ม</string> - <string name="info_row_local_name">ชื่อภายในเครื่องเท่านั้น</string> + <string name="info_row_local_name">ชื่อภายในเครื่อง</string> <string name="users_delete_data_only">ข้อมูลโปรไฟล์ภายในเครื่องเท่านั้น</string> <string name="make_profile_private">ทำให้โปรไฟล์เป็นส่วนตัว!</string> <string name="incognito">ไม่ระบุตัวตน</string> @@ -623,9 +620,9 @@ <string name="import_theme">นำเข้าธีม</string> <string name="import_theme_error">การนำเข้าธีมผิดพลาด</string> <string name="theme_light">สว่าง</string> - <string name="message_deletion_prohibited_in_chat">การลบข้อความแบบกลับไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้</string> + <string name="message_deletion_prohibited_in_chat">การลบข้อความแบบแก้ไขไม่ได้เป็นสิ่งที่ห้ามในกลุ่มนี้</string> <string name="v4_3_improved_privacy_and_security">ปรับปรุงความเป็นส่วนตัวและความปลอดภัยแล้ว</string> - <string name="v4_3_improved_server_configuration">ปรับปรุงการตั้งค่าเซิร์ฟเวอร์แล้ว</string> + <string name="v4_3_improved_server_configuration">ปรับปรุงการกําหนดค่าเซิร์ฟเวอร์แล้ว</string> <string name="v4_4_live_messages">ข้อความสด</string> <string name="v5_1_japanese_portuguese_interface">UI ภาษาญี่ปุ่นและโปรตุเกส</string> <string name="opening_database">กำลังเปิดฐานข้อมูล…</string> @@ -656,24 +653,23 @@ <string name="only_group_owners_can_enable_voice">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานข้อความเสียงได้</string> <string name="no_details">ไม่มีรายละเอียด</string> <string name="ok">ตกลง</string> - <string name="add_contact">ลิงก์คำเชิญแบบครั้งเดียว</string> + <string name="add_contact">ลิงก์คําเชิญแบบใช้ครั้งเดียว</string> <string name="only_stored_on_members_devices">(จัดเก็บโดยสมาชิกในกลุ่มเท่านั้น)</string> <string name="toast_permission_denied">ปฏิเสธการอนุญาต!</string> <string name="mark_read">ทำเครื่องหมายอ่านแล้ว</string> <string name="mark_unread">ทำเครื่องหมายว่ายังไม่ได้อ่าน</string> <string name="mute_chat">ปิดเสียง</string> <string name="icon_descr_more_button">เพิ่มเติม</string> - <string name="paste_connection_link_below_to_connect">แปะลิงก์ที่คุณได้รับลงในช่องด้านล่างเพื่อเชื่อมต่อกับผู้ติดต่อของคุณ</string> <string name="paste_button">แปะ</string> <string name="one_time_link">ลิงก์คําเชิญแบบใช้ครั้งเดียว</string> - <string name="mark_code_verified">ทำเครื่องหมายยืนยัน</string> + <string name="mark_code_verified">ทําเครื่องหมายว่ายืนยันแล้ว</string> <string name="markdown_help">ช่วยมาร์คดาวน์</string> <string name="markdown_in_messages">Markdown ในข้อความ</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">ตรวจสอบให้แน่ใจว่าที่อยู่เซิร์ฟเวอร์ WebRTC ICE อยู่ในรูปแบบที่ถูกต้อง แยกบรรทัดและไม่ซ้ำกัน</string> <string name="network_and_servers">เครือข่ายและเซิร์ฟเวอร์</string> <string name="network_settings_title">การตั้งค่าเครือข่าย</string> <string name="network_use_onion_hosts_required_desc">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ</string> - <string name="network_use_onion_hosts_no">เลขที่</string> + <string name="network_use_onion_hosts_no">ไม่</string> <string name="network_use_onion_hosts_required_desc_in_alert">จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">โฮสต์หัวหอมจะถูกใช้เมื่อมี</string> <string name="network_use_onion_hosts_no_desc">โฮสต์หัวหอมจะไม่ถูกใช้</string> @@ -690,7 +686,7 @@ <string name="open_verb">เปิด</string> <string name="open_simplex_chat_to_accept_call">เปิด SimpleX Chat เพื่อรับสาย</string> <string name="call_connection_peer_to_peer">เพื่อนต่อเพื่อน</string> - <string name="icon_descr_call_pending_sent">รอสาย</string> + <string name="icon_descr_call_pending_sent">รอดำเนินการสาย</string> <string name="icon_descr_call_missed">สายที่ไม่ได้รับ</string> <string name="new_passcode">รหัสผ่านใหม่</string> <string name="la_mode_passcode">รหัสผ่าน</string> @@ -714,9 +710,9 @@ <string name="group_member_role_observer">ผู้สังเกตการณ์</string> <string name="group_member_role_member">สมาชิก</string> <string name="new_member_role">บทบาทของสมาชิกใหม่</string> - <string name="no_contacts_to_add">ไม่มีรายชื่อที่จะเพิ่ม</string> + <string name="no_contacts_to_add">ไม่มีผู้ติดต่อที่จะเพิ่ม</string> <string name="no_contacts_selected">ไม่ได้เลือกผู้ติดต่อ</string> - <string name="only_group_owners_can_change_prefs">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนการตั้งค่ากลุ่มได้</string> + <string name="only_group_owners_can_change_prefs">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้</string> <string name="share_text_moderated_at">กลั่นกรองที่: %s</string> <string name="info_row_moderated_at">กลั่นกรองที่</string> <string name="item_info_no_text">ไม่มีข้อความ</string> @@ -727,27 +723,27 @@ <string name="network_option_ping_interval">ช่วงเวลา PING</string> <string name="user_mute">ปิดเสียง</string> <string name="muted_when_inactive">ปิดเสียงเมื่อไม่ได้ใช้งาน!</string> - <string name="import_theme_error_desc">ตรวจสอบว่าไฟล์มีไวยากรณ์ YAML ที่ถูกต้อง ส่งออกธีมเพื่อให้มีตัวอย่างโครงสร้างไฟล์ธีม</string> + <string name="import_theme_error_desc">ตรวจสอบให้แน่ใจว่าไฟล์มีไวยากรณ์ YAML ที่ถูกต้อง ส่งออกธีมเพื่อให้มีตัวอย่างโครงสร้างไฟล์ธีม</string> <string name="color_surface">เมนู & การแจ้งเตือน</string> <string name="chat_preferences_on">เปิด</string> - <string name="chat_preferences_no">เลขที่</string> + <string name="chat_preferences_no">ไม่</string> <string name="chat_preferences_off">ปิด</string> - <string name="message_reactions">ปฏิกิริยาของข้อความ</string> + <string name="message_reactions">ปฏิกิริยาต่อข้อความ</string> <string name="feature_off">ปิด</string> <string name="only_you_can_send_disappearing">มีเพียงคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</string> <string name="only_your_contact_can_send_disappearing">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages) ได้</string> <string name="only_you_can_delete_messages">มีเพียงคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (ผู้ติดต่อของคุณสามารถทำเครื่องหมายเพื่อลบได้)</string> <string name="only_you_can_send_voice">มีเพียงคุณเท่านั้นที่สามารถส่งข้อความเสียงได้</string> <string name="only_your_contact_can_delete">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถลบข้อความแบบย้อนกลับไม่ได้ (คุณสามารถทำเครื่องหมายเพื่อลบได้)</string> - <string name="message_reactions_prohibited_in_this_chat">ห้ามแสดงปฏิกิริยาบนข้อความในแชทนี้</string> + <string name="message_reactions_prohibited_in_this_chat">ห้ามแสดงปฏิกิริยาต่อข้อความในแชทนี้</string> <string name="only_you_can_add_message_reactions">มีเพียงคุณเท่านั้นที่สามารถแสดงปฏิกิริยาต่อข้อความได้</string> - <string name="only_your_contact_can_add_message_reactions">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มการโต้ตอบข้อความได้</string> + <string name="only_your_contact_can_add_message_reactions">เฉพาะผู้ติดต่อของคุณเท่านั้นที่สามารถเพิ่มปฏิกิริยาต่อข้อความได้</string> <string name="only_you_can_make_calls">มีเพียงคุณเท่านั้นที่โทรออกได้</string> <string name="only_your_contact_can_make_calls">ผู้ติดต่อของคุณเท่านั้นที่สามารถโทรออกได้</string> - <string name="message_reactions_are_prohibited">ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้</string> - <string name="feature_offered_item">เสนอ %s</string> + <string name="message_reactions_are_prohibited">ปฏิกิริยาต่อข้อความป็นสิ่งต้องห้ามในกลุ่มนี้</string> + <string name="feature_offered_item">เสนอแล้ว %s</string> <string name="new_in_version">ใหม่ใน %s</string> - <string name="feature_offered_item_with_param">เสนอ %s: %2s</string> + <string name="feature_offered_item_with_param">เสนอแล้ว %s: %2s</string> <string name="v4_3_voice_messages_desc">สูงสุด 40 วินาที รับทันที</string> <string name="v4_5_multiple_chat_profiles">โปรไฟล์การแชทหลายรายการ</string> <string name="v4_5_message_draft">ร่างข้อความ</string> @@ -756,7 +752,7 @@ <string name="v4_6_group_moderation_descr">ขณะนี้ผู้ดูแลระบบสามารถ: \n- ลบข้อความของสมาชิก \n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\")</string> - <string name="v5_1_message_reactions">ปฏิกิริยาของข้อความ</string> + <string name="v5_1_message_reactions">ปฏิกิริยาต่อข้อความ</string> <string name="custom_time_unit_minutes">นาที</string> <string name="custom_time_unit_months">เดือน</string> <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 มือถือ: แตะ <b>เปิดในแอปมือถือ</b> จากนั้นแตะ <b>เชื่อมต่อ</b> ในแอป]]></string> @@ -807,11 +803,11 @@ <string name="prohibit_calls">ห้ามการโทรด้วยเสียง/วิดีโอ</string> <string name="prohibit_message_deletion">ห้ามการลบข้อความที่ย้อนกลับไม่ได้</string> <string name="prohibit_message_reactions_group">ห้ามแสดงปฏิกิริยาต่อข้อความ</string> - <string name="prohibit_direct_messages">ห้ามส่งข้อความส่วนตัวถึงสมาชิก</string> + <string name="prohibit_direct_messages">ห้ามส่งข้อความโดยตรงถึงสมาชิก</string> <string name="prohibit_sending_disappearing">ห้ามส่งข้อความที่จะหายไปหลังจากเวลาที่กำหนดหลังการอ่าน (disappearing messages)</string> <string name="prohibit_sending_voice">ห้ามส่งข้อความเสียง</string> <string name="whats_new_read_more">อ่านเพิ่มเติม</string> - <string name="v4_5_message_draft_descr">เก็บร่างข้อความล่าสุดพร้อมไฟล์แนบ</string> + <string name="v4_5_message_draft_descr">เก็บข้อความที่ร่างไว้ล่าสุดพร้อมไฟล์แนบ</string> <string name="v4_5_private_filenames">ชื่อไฟล์ส่วนตัว</string> <string name="v4_6_hidden_chat_profiles_descr">ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน!</string> <string name="v5_0_polish_interface">อินเตอร์เฟซภาษาโปแลนด์</string> @@ -860,7 +856,7 @@ <string name="remove_member_confirmation">ลบ</string> <string name="role_in_group">บทบาท</string> <string name="save_and_update_group_profile">บันทึกและอัปเดตโปรไฟล์กลุ่ม</string> - <string name="receiving_via">กำลังจะรับทาง</string> + <string name="receiving_via">กำลังรับผ่าน</string> <string name="network_options_reset_to_defaults">รีเซ็ตเป็นค่าเริ่มต้น</string> <string name="network_options_revert">เปลี่ยนกลับ</string> <string name="network_options_save">บันทึก</string> @@ -881,7 +877,8 @@ <string name="self_destruct_passcode_enabled">เปิดใช้งานรหัสผ่านแบบทําลายตัวเองแล้ว!</string> <string name="send_disappearing_message_send">ส่ง</string> <string name="send_live_message_desc">ส่งข้อความสด - มันจะอัปเดตสําหรับผู้รับในขณะที่คุณพิมพ์</string> - <string name="send_disappearing_message">ส่งข้อความแบบที่หายไป</string> + <string name="send_disappearing_message">ข้อความที่จะหายไปหลังเวลาที่กําหนด +\n(disappearing message)</string> <string name="sender_cancelled_file_transfer">ผู้ส่งยกเลิกการโอนไฟล์</string> <string name="info_row_sent_at">ส่งเมื่อ</string> <string name="share_message">แชร์ข้อความ…</string> @@ -895,12 +892,12 @@ <string name="self_destruct_passcode_changed">รหัสผ่านแบบทําลายตัวเองเปลี่ยนไป!</string> <string name="set_passcode">ตั้งรหัสผ่าน</string> <string name="settings_section_title_settings">การตั้งค่า</string> - <string name="set_password_to_export">ตั้งค่ารหัสผ่านเพื่อส่งออก</string> + <string name="set_password_to_export">ตั้งรหัสผ่านเพื่อส่งออก</string> <string name="current_version_timestamp">%s (ปัจจุบัน)</string> <string name="sending_via">ส่งผ่าน</string> <string name="set_group_preferences">ตั้งค่าการกําหนดลักษณะกลุ่ม</string> <string name="sending_files_not_yet_supported">การส่งไฟล์ยังไม่ได้รับการรองรับ</string> - <string name="sender_may_have_deleted_the_connection_request">ผู้ส่งอาจลบคําขอการเชื่อมต่อ</string> + <string name="sender_may_have_deleted_the_connection_request">ผู้ส่งอาจลบคําขอการเชื่อมต่อแล้ว</string> <string name="smp_server_test_secure_queue">คิวที่ปลอดภัย</string> <string name="notification_preview_mode_message_desc">แสดงที่ติดต่อและข้อความ</string> <string name="notification_preview_mode_contact_desc">แสดงเฉพาะผู้ติดต่อ</string> @@ -908,7 +905,7 @@ <string name="sent_message">ข้อความที่ส่งแล้ว</string> <string name="stop_snd_file__message">การส่งไฟล์จะหยุดลง</string> <string name="icon_descr_sent_msg_status_send_failed">ส่งล้มเหลว</string> - <string name="icon_descr_sent_msg_status_sent">ส่ง</string> + <string name="icon_descr_sent_msg_status_sent">ส่งแล้ว</string> <string name="share_image">แชร์สื่อ…</string> <string name="icon_descr_send_message">ส่งข้อความ</string> <string name="send_live_message">ส่งข้อความสด</string> @@ -927,7 +924,7 @@ <string name="smp_servers_scan_qr">สแกนคิวอาร์โค้ดของเซิร์ฟเวอร์</string> <string name="smp_save_servers_question">บันทึกเซิร์ฟเวอร์\?</string> <string name="saved_ICE_servers_will_be_removed">เซิร์ฟเวอร์ WebRTC ICE ที่บันทึกไว้จะถูกลบออก</string> - <string name="disable_onion_hosts_when_not_supported"><![CDATA[ตั้งค่า <i>ใช้โฮสต์ .onion</i> เป็น ไม่ หากพร็อกซี SOCKS ไม่รองรับ]]></string> + <string name="disable_onion_hosts_when_not_supported"><![CDATA[ตั้ง <i>ใช้โฮสต์ .onion</i> เป็น ไม่ หากพร็อกซี SOCKS ไม่รองรับ]]></string> <string name="show_dev_options">แสดง:</string> <string name="show_developer_options">แสดงตัวเลือกสําหรับนักพัฒนาซอฟต์แวร์</string> <string name="share_link">แชร์ลิงก์</string> @@ -949,8 +946,8 @@ <string name="color_secondary">รอง</string> <string name="accept_feature_set_1_day">ตั้ง 1 วัน</string> <string name="v4_4_disappearing_messages_desc">ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด</string> - <string name="v4_6_group_welcome_message_descr">ตั้งค่าข้อความที่แสดงต่อสมาชิกใหม่!</string> - <string name="v5_0_app_passcode_descr">ตั้งค่าแทนการรับรองความถูกต้องของระบบ</string> + <string name="v4_6_group_welcome_message_descr">ตั้งข้อความที่แสดงต่อสมาชิกใหม่!</string> + <string name="v5_0_app_passcode_descr">ตั้งแทนการรับรองความถูกต้องของระบบ</string> <string name="v5_1_self_destruct_passcode">รหัสผ่านแบบทําลายตัวเอง</string> <string name="custom_time_picker_select">เลือก</string> <string name="theme_simplex">SimpleX</string> @@ -958,7 +955,7 @@ <string name="non_fatal_errors_occured_during_import">ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat</string> <string name="star_on_github">ติดดาวบน GitHub</string> <string name="switch_verb">เปลี่ยน</string> - <string name="callstate_starting">เริ่มต้น…</string> + <string name="callstate_starting">กำลังเริ่มต้น…</string> <string name="stop_chat_to_enable_database_actions">หยุดการแชทเพื่อเปิดใช้งานการดำเนินการกับฐานข้อมูล</string> <string name="stop_chat_to_export_import_or_delete_chat_database">หยุดแชทเพื่อส่งออก นำเข้า หรือลบฐานข้อมูลแชท คุณจะไม่สามารถรับและส่งข้อความได้ในขณะที่การแชทหยุดลง</string> <string name="v4_6_audio_video_calls_descr">รองรับบลูทูธและการปรับปรุงอื่นๆ</string> @@ -985,7 +982,7 @@ <string name="ntf_channel_calls">การโทร SimpleX Chat</string> <string name="ntf_channel_messages">ข้อความแชท SimpleX</string> <string name="settings_notification_preview_mode_title">แสดงตัวอย่าง</string> - <string name="notifications_mode_periodic">เริ่มเป็นช่วงๆ</string> + <string name="notifications_mode_periodic">เริ่มเป็นระยะ</string> <string name="la_notice_title_simplex_lock">ล็อค SimpleX</string> <string name="la_lock_mode">โหมดล็อค SimpleX</string> <string name="la_lock_mode_system">การรับรองความถูกต้องของระบบ</string> @@ -1001,17 +998,17 @@ <string name="add_contact_or_create_group">เริ่มแชทใหม่</string> <string name="thank_you_for_installing_simplex">ขอบคุณสำหรับการติดตั้ง SimpleX Chat!</string> <string name="chat_help_tap_button">แตะปุ่ม</string> - <string name="connection_you_accepted_will_be_cancelled">การเชื่อมต่อที่คุณยอมรับจะถูกยกเลิก!</string> + <string name="connection_you_accepted_will_be_cancelled">การเชื่อมต่อที่คุณยอมรับแล้วจะถูกยกเลิก!</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">ผู้ติดต่อที่คุณแชร์ลิงก์นี้ด้วยจะไม่สามารถเชื่อมต่อได้!</string> <string name="show_QR_code">แสดงคิวอาร์โค้ด</string> <string name="simplex_address">ที่อยู่ SimpleX</string> <string name="is_not_verified">%s ไม่ได้รับการยืนยัน</string> - <string name="is_verified">%s ได้รับการตรวจสอบแล้ว</string> + <string name="is_verified">%s ได้รับการยืนยันแล้ว</string> <string name="chat_lock">ล็อค SimpleX</string> <string name="smp_servers">เซิร์ฟเวอร์ SMP</string> <string name="smp_servers_test_server">เซิร์ฟเวอร์ทดสอบ</string> <string name="smp_servers_test_servers">เซิร์ฟเวอร์ทดสอบ</string> - <string name="smp_servers_test_some_failed">บางเซิร์ฟเวอร์ไม่ผ่านการทดสอบ:</string> + <string name="smp_servers_test_some_failed">บางเซิร์ฟเวอร์ผ่านการทดสอบล้มเหลว:</string> <string name="network_socks_proxy_settings">การตั้งค่าพร็อกซี SOCKS</string> <string name="core_simplexmq_version">Simplexmq: v%s (%2s)</string> <string name="first_platform_without_user_ids">แพลตฟอร์มแรกที่ไม่มีตัวระบุผู้ใช้ - ถูกออกแบบให้เป็นส่วนตัว</string> @@ -1020,7 +1017,7 @@ <string name="alert_title_skipped_messages">ข้อความที่ข้ามไป</string> <string name="submit_passcode">ส่ง</string> <string name="la_mode_system">ระบบ</string> - <string name="settings_section_title_support">สนับสนุนแชท SIMPLEX</string> + <string name="settings_section_title_support">สนับสนุน SIMPLEX แชท</string> <string name="settings_section_title_socks">พร็อกซี SOCKS</string> <string name="stop_chat_confirmation">หยุด</string> <string name="stop_chat_question">หยุดแชท\?</string> @@ -1034,9 +1031,9 @@ <string name="v5_0_polish_interface_descr">ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</string> <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">แพลตฟอร์มการส่งข้อความและแอปพลิเคชันที่ปกป้องความเป็นส่วนตัวและความปลอดภัยของคุณ</string> <string name="next_generation_of_private_messaging">การส่งข้อความส่วนตัวรุ่นต่อไป</string> - <string name="member_role_will_be_changed_with_notification">บทบาทจะเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง</string> + <string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง</string> <string name="delete_files_and_media_desc">การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</string> - <string name="to_reveal_profile_enter_password">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ</string> + <string name="to_reveal_profile_enter_password">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ</string> <string name="network_session_mode_transport_isolation">การแยกการขนส่ง</string> <string name="unhide_chat_profile">ยกเลิกการซ่อนโปรไฟล์การแชท</string> <string name="update_database">อัปเดต</string> @@ -1074,10 +1071,10 @@ <string name="unknown_message_format">รูปแบบข้อความที่ไม่รู้จัก</string> <string name="sender_you_pronoun">คุณ</string> <string name="description_via_group_link">ผ่านลิงค์กลุ่ม</string> - <string name="description_via_one_time_link">ผ่านลิงค์แบบครั้งเดียว</string> + <string name="description_via_one_time_link">ผ่านลิงค์แบบใช้ครั้งเดียว</string> <string name="description_via_contact_address_link">ผ่านลิงค์ที่อยู่ติดต่อ</string> <string name="description_you_shared_one_time_link">คุณแชร์ลิงก์แบบใช้ครั้งเดียว</string> - <string name="description_you_shared_one_time_link_incognito">คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตน</string> + <string name="description_you_shared_one_time_link_incognito">คุณแชร์ลิงก์แบบใช้ครั้งเดียวโดยไม่ระบุตัวตนแล้ว</string> <string name="simplex_link_mode_browser">ผ่านเบราว์เซอร์</string> <string name="simplex_link_connection">ผ่าน %1$s</string> <string name="failed_to_create_user_duplicate_desc">คุณมีโปรไฟล์แชทที่ใช้ชื่อแสดงเดียวกันอยู่แล้ว กรุณาเลือกชื่ออื่น</string> @@ -1095,7 +1092,7 @@ <string name="you_can_turn_on_lock">คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า</string> <string name="moderate_message_will_be_deleted_warning">ข้อความจะถูกลบสำหรับสมาชิกทั้งหมด</string> <string name="moderate_message_will_be_marked_warning">ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน</string> - <string name="icon_descr_received_msg_status_unread">เปลี่ยนเป็นยังไม่ได้อ่าน</string> + <string name="icon_descr_received_msg_status_unread">ยังไม่ได้อ่าน</string> <string name="icon_descr_sent_msg_status_unauthorized_send">ส่งโดยไม่ได้รับอนุญาต</string> <string name="welcome">ยินดีต้อนรับ!</string> <string name="personal_welcome">ยินดีต้อนรับ %1$s!</string> @@ -1103,7 +1100,7 @@ <string name="you_have_no_chats">คุณไม่มีการแชท</string> <string name="your_chats">แชท</string> <string name="you_are_observer">คุณเป็นผู้สังเกตการณ์</string> - <string name="image_decoding_exception_desc">ไม่สามารถถอดรหัสภาพได้ โปรดลองใช้ภาพอื่นหรือติดต่อผู้พัฒนาแอป</string> + <string name="image_decoding_exception_desc">ภาพไม่สามารถถอดรหัส ได้ โปรดลองใช้รูปภาพอื่นหรือติดต่อนักพัฒนา</string> <string name="observer_cant_send_message_title">คุณไม่สามารถส่งข้อความได้!</string> <string name="icon_descr_waiting_for_image">กําลังรอภาพ</string> <string name="waiting_for_image">กําลังรอภาพ</string> @@ -1126,8 +1123,8 @@ <string name="to_start_a_new_chat_help_header">เพื่อเริ่มแชทใหม่</string> <string name="gallery_video_button">วิดีโอ</string> <string name="unmute_chat">เปิดเสียง</string> - <string name="you_invited_your_contact">คุณได้เชิญผู้ติดต่อของคุณ</string> - <string name="you_accepted_connection">คุณยอมรับการเชื่อมต่อ</string> + <string name="you_invited_a_contact">คุณได้เชิญผู้ติดต่อของคุณ</string> + <string name="you_accepted_connection">คุณยอมรับการเชื่อมต่อแล้ว</string> <string name="contact_wants_to_connect_with_you">ต้องการเชื่อมต่อกับคุณ!</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์ \nคุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่)</string> @@ -1139,7 +1136,6 @@ <string name="your_chat_profile_will_be_sent_to_your_contact">โปรไฟล์แชทของคุณจะถูกส่ง \nให้กับผู้ติดต่อของคุณ</string> <string name="you_will_be_connected_when_your_contacts_device_is_online">คุณจะเชื่อมต่อเมื่ออุปกรณ์ของผู้ติดต่อของคุณออนไลน์อยู่ โปรดรอหรือตรวจสอบภายหลัง!</string> - <string name="your_profile_will_be_sent">โปรไฟล์การแชทของคุณจะถูกส่งไปยังผู้ติดต่อของคุณ</string> <string name="you_can_accept_or_reject_connection">เมื่อมีคนขอเชื่อมต่อ คุณสามารถยอมรับหรือปฏิเสธได้</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[คุณสามารถเชื่อมต่อได้โดยคลิกที่ลิงค์ หากเปิดในเบราว์เซอร์ ให้คลิกปุ่ม <b>เปิดในแอปมือถือ</b>]]></string> <string name="to_verify_compare">ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ</string> @@ -1162,7 +1158,7 @@ <string name="network_use_onion_hosts">ใช้โฮสต์ .onion</string> <string name="network_use_onion_hosts_prefer">เมื่อพร้อมใช้งาน</string> <string name="update_network_session_mode_question">อัปเดตโหมดการแยกการขนส่งไหม\?</string> - <string name="theme_colors_section_title">ธีมสี</string> + <string name="theme_colors_section_title">สีของธีม</string> <string name="your_contacts_will_remain_connected">ผู้ติดต่อของคุณจะยังคงเชื่อมต่ออยู่</string> <string name="you_can_create_it_later">คุณสามารถสร้างได้ในภายหลัง</string> <string name="your_contacts_will_see_it">ผู้ติดต่อของคุณใน SimpleX จะเห็น @@ -1216,7 +1212,7 @@ <string name="button_welcome_message">ข้อความต้อนรับ</string> <string name="you_can_share_group_link_anybody_will_be_able_to_connect">คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</string> <string name="you_can_share_this_address_with_your_contacts">คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ %s</string> - <string name="member_role_will_be_changed_with_invitation">บทบาทจะเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่</string> + <string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่</string> <string name="group_welcome_title">ข้อความต้อนรับ</string> <string name="group_main_profile_sent">โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</string> <string name="update_network_settings_confirmation">อัปเดต</string> @@ -1235,23 +1231,22 @@ <string name="voice_prohibited_in_this_chat">ห้ามส่งข้อความเสียงในแชทนี้</string> <string name="voice_messages_are_prohibited">ข้อความเสียงเป็นสิ่งต้องห้ามในกลุ่มนี้</string> <string name="v4_3_voice_messages">ข้อความเสียง</string> - <string name="v4_2_auto_accept_contact_requests_desc">พร้อมข้อความต้อนรับเพิ่มเติม</string> + <string name="v4_2_auto_accept_contact_requests_desc">พร้อมข้อความต้อนรับที่ไม่บังคับ</string> <string name="v4_3_irreversible_message_deletion_desc">ผู้ติดต่อของคุณสามารถอนุญาตให้ลบข้อความทั้งหมดได้</string> <string name="v4_5_transport_isolation">การแยกการขนส่ง</string> <string name="v4_5_private_filenames_descr">ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา</string> <string name="v5_0_large_files_support">วิดีโอและไฟล์สูงสุด 1gb</string> <string name="custom_time_unit_weeks">สัปดาห์</string> <string name="delete_chat_profile_action_cannot_be_undone_warning">การดำเนินการนี้ไม่สามารถยกเลิกได้ - โปรไฟล์ ผู้ติดต่อ ข้อความ และไฟล์ของคุณจะสูญหายไปอย่างถาวร</string> - <string name="incognito_info_find">หากต้องการค้นหาโปรไฟล์ที่ใช้สำหรับการเชื่อมต่อแบบไม่ระบุตัวตน ให้แตะชื่อผู้ติดต่อหรือชื่อกลุ่มที่ด้านบนของแชท</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[เพื่อรักษาความเป็นส่วนตัวของคุณ แทนที่จะใช้การแจ้งเตือนแบบพุช แอปมีบริการพื้นหลัง <b>SimpleX </b> – ใช้แบตเตอรี่เพียงไม่กี่เปอร์เซ็นต์ต่อวัน]]></string> <string name="group_info_member_you">คุณ: %1$s</string> <string name="to_protect_privacy_simplex_has_ids_for_queues">เพื่อปกป้องความเป็นส่วนตัว แทนที่จะใช้ ID ผู้ใช้เหมือนที่แพลตฟอร์มอื่นๆใช้ SimpleX มีตัวระบุสำหรับคิวข้อความ โดยแยกจากกันสำหรับผู้ติดต่อแต่ละราย</string> - <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">คุณจะต้องยืนยันตัวตนเมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง</string> + <string name="auth_you_will_be_required_to_authenticate_when_you_start_or_resume">คุณจะต้องตรวจสอบสิทธิ์เมื่อคุณเริ่มหรือกลับมาใช้แอปพลิเคชันอีกครั้งหลังจากผ่านไป 30 วินาทีในพื้นหลัง</string> <string name="you_will_join_group">คุณจะเข้าร่วมกลุ่มที่ลิงก์นี้อ้างถึงและเชื่อมต่อกับสมาชิกในกลุ่ม</string> - <string name="this_text_is_available_in_settings">ตัวบทนี้มีอยู่ในการตั้งค่า</string> + <string name="this_text_is_available_in_settings">ข้อความนี้มีอยู่ในการตั้งค่า</string> <string name="images_limit_title">รูปเยอะเกิน!</string> <string name="videos_limit_title">วิดีโอเยอะเกิน!</string> - <string name="switch_receiving_address_desc">คุณลักษณะนี้เป็นการทดลอง! จะใช้งานได้ก็ต่อเมื่อลูกค้าอื่นติดตั้งเวอร์ชัน 4.2 คุณควรเห็นข้อความในการสนทนาเมื่อการเปลี่ยนแปลงที่อยู่เสร็จสิ้น – โปรดตรวจสอบว่าคุณยังคงสามารถรับข้อความจากผู้ติดต่อนี้ (หรือสมาชิกในกลุ่ม)</string> + <string name="switch_receiving_address_desc">ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์</string> <string name="to_connect_via_link_title">เพื่อการเชื่อมต่อผ่านลิงค์</string> <string name="this_link_is_not_a_valid_connection_link">ลิงค์นี้ไม่ใช่ลิงค์เชื่อมต่อที่ถูกต้อง!</string> <string name="this_QR_code_is_not_a_link">รหัสคิวอาร์นี้ไม่ใช่ลิงก์!</string> @@ -1266,7 +1261,7 @@ <string name="search_verb">ค้นหา</string> <string name="la_mode_off">ปิด</string> <string name="files_and_media_prohibited">ไฟล์และสื่อต้องห้าม!</string> - <string name="no_filtered_chats">ไม่มีการกรองการแชท</string> + <string name="no_filtered_chats">ไม่มีการแชทที่กรองแล้ว</string> <string name="abort_switch_receiving_address_confirm">ยกเลิก</string> <string name="abort_switch_receiving_address_question">ยกเลิกการเปลี่ยนที่อยู่\?</string> <string name="favorite_chat">ที่ชอบ</string> @@ -1282,6 +1277,81 @@ <string name="error_aborting_address_change">ข้อผิดพลาดในการยกเลิกการเปลี่ยนที่อยู่</string> <string name="abort_switch_receiving_address_desc">การเปลี่ยนแปลงที่อยู่จะถูกยกเลิก จะใช้ที่อยู่เก่าของผู้รับ</string> <string name="only_owners_can_enable_files_and_media">เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปิดใช้งานไฟล์และสื่อได้</string> - <string name="conn_event_ratchet_sync_started">เห็นด้วยกับการ encrypt…</string> - <string name="snd_conn_event_ratchet_sync_started">ตกลงการ encryption สำหรับ %s</string> + <string name="conn_event_ratchet_sync_started">เห็นด้วยกับการ encryption…</string> + <string name="snd_conn_event_ratchet_sync_started">กำลังตกลงการ encryption สำหรับ %s</string> + <string name="receipts_section_contacts">ติดต่อ</string> + <string name="receipts_contacts_title_enable">เปิดใช้งานใบเสร็จ\?</string> + <string name="receipts_contacts_override_enabled">การส่งใบเสร็จถูกเปิดใช้งานสำหรับผู้ติดต่อ %d</string> + <string name="snd_conn_event_ratchet_sync_agreed">ตกลง encryption สำหรับ %s</string> + <string name="sender_at_ts">%s ที่ %s</string> + <string name="fix_connection">แก้ไขการเชื่อมต่อ</string> + <string name="v5_2_fix_encryption">รักษาการเชื่อมต่อของคุณ</string> + <string name="v5_2_favourites_filter_descr">กรองแชทที่ยังไม่อ่านและแชทโปรด</string> + <string name="delivery_receipts_are_disabled">ใบตอบรับการจัดส่งถูกปิดใช้งาน!</string> + <string name="dont_enable_receipts">อย่าเปิดใช้งาน</string> + <string name="sending_delivery_receipts_will_be_enabled">การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิด</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">การส่งใบเสร็จรับการจัดส่งข้อความจะถูกเปิดในโปรไฟล์แชทที่มองเห็นได้ทั้งหมด</string> + <string name="you_can_enable_delivery_receipts_later">คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า</string> + <string name="error_enabling_delivery_receipts">เกิดข้อผิดพลาดในการเปิดใช้ใบเสร็จการจัดส่ง!</string> + <string name="choose_file_title">เลือกไฟล์</string> + <string name="receipts_contacts_title_disable">ปิดใช้งานใบเสร็จ\?</string> + <string name="receipts_contacts_disable_for_all">ปิดการใช้งานสำหรับทุกคน</string> + <string name="receipts_contacts_enable_for_all">เปิดใช้งานสําหรับทุกคน</string> + <string name="settings_section_title_delivery_receipts">ส่งใบเสร็จรับการจัดส่งข้อความไปที่</string> + <string name="conn_event_ratchet_sync_allowed">อนุญาตให้มีการเจรจา encryption อีกครั้ง</string> + <string name="snd_conn_event_ratchet_sync_ok">encryptionใช้ได้สำหรับ %s</string> + <string name="conn_event_ratchet_sync_required">จำเป็นต้องมีการเจรจา encryption อีกครั้ง</string> + <string name="renegotiate_encryption">เจรจา encryption อีกครั้ง</string> + <string name="v5_2_favourites_filter">ค้นหาแชทได้เร็วขึ้น</string> + <string name="v5_2_message_delivery_receipts_descr">ขีดที่สองที่เราพลาด! ✅</string> + <string name="delivery_receipts_title">ใบตอบรับการจัดส่ง!</string> + <string name="in_developing_title">เร็วๆ นี้!</string> + <string name="snd_conn_event_ratchet_sync_allowed">อนุญาตให้มีการเจรจา encryption อีกครั้งสําหรับ %s</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="receipts_section_description">การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ</string> + <string name="receipts_section_description_1">สามารถลบล้างได้ในการตั้งค่าผู้ติดต่อและกลุ่ม</string> + <string name="receipts_contacts_disable_keep_overrides">ปิดใช้งาน (เก็บการแทนที่)</string> + <string name="receipts_contacts_enable_keep_overrides">เปิดใช้งาน (เก็บการแทนที่)</string> + <string name="conn_event_ratchet_sync_agreed">ตกลง encryption</string> + <string name="fix_connection_confirm">แก้ไข</string> + <string name="v5_2_more_things_descr">- การส่งข้อความมีเสถียรภาพมากขึ้น +\n- กลุ่มที่ดีขึ้นเล็กน้อย +\n- และอื่น ๆ!</string> + <string name="receipts_groups_enable_for_all">เปิดใช้งานสำหรับทุกกลุ่ม</string> + <string name="sync_connection_force_desc">encryption กำลังทำงานและไม่จำเป็นต้องใช้ข้อตกลง encryption ใหม่ อาจทำให้การเชื่อมต่อผิดพลาดได้!</string> + <string name="sync_connection_force_confirm">เจรจาใหม่</string> + <string name="enable_receipts_all">เปิดใช้งาน</string> + <string name="v5_2_more_things">อีกสองสามอย่าง</string> + <string name="delivery">จัดส่ง</string> + <string name="receipts_groups_title_disable">ปิดรับใบเสร็จสำหรับกลุ่มไหม</string> + <string name="send_receipts_disabled">ปิดการใช้งาน</string> + <string name="receipts_groups_disable_for_all">ปิดการใช้งานสำหรับทุกกลุ่ม</string> + <string name="receipts_groups_enable_keep_overrides">เปิดใช้งาน (เก็บการแทนที่กลุ่ม)</string> + <string name="receipts_groups_title_enable">เปิดใช้ใบเสร็จสำหรับกลุ่มไหม</string> + <string name="receipts_groups_disable_keep_overrides">ปิดใช้งาน (เก็บการแทนที่กลุ่ม)</string> + <string name="snd_conn_event_ratchet_sync_required">จำเป็นต้องมีการเจรจา encryption อีกครั้งสำหรับ %s</string> + <string name="v5_2_disappear_one_message_descr">แม้ในขณะที่ปิดใช้งานในการสนทนา</string> + <string name="fix_connection_question">แก้ไขการเชื่อมต่อ\?</string> + <string name="v5_2_message_delivery_receipts">ใบเสร็จการส่งข้อความ!</string> + <string name="fix_connection_not_supported_by_contact">การแก้ไขไม่รองรับโดยผู้ติดต่อ</string> + <string name="fix_connection_not_supported_by_group_member">การแก้ไขไม่สนับสนุนโดยสมาชิกกลุ่ม</string> + <string name="v5_2_disappear_one_message">ทำให้ข้อความหายไปหนึ่งข้อความ</string> + <string name="no_info_on_delivery">ไม่มีข้อมูลเกี่ยวกับการจัดส่ง</string> + <string name="no_selected_chat">ไม่มีแชทที่เลือก</string> + <string name="send_receipts_disabled_alert_title">ใบเสร็จรับข้อความถูกปิดใช้งาน</string> + <string name="rcv_conn_event_verification_code_reset">เปลี่ยนรหัสความปลอดภัยแล้ว</string> + <string name="receipts_groups_override_disabled">การส่งใบเสร็จถูกปิดใช้งานสำหรับกลุ่ม %d</string> + <string name="receipts_contacts_override_disabled">การส่งใบเสร็จถูกปิดใช้งานสำหรับผู้ติดต่อ %d</string> + <string name="receipts_groups_override_enabled">เปิดใช้งานการส่งใบเสร็จสำหรับกลุ่ม %d</string> + <string name="send_receipts">ส่งใบเสร็จ</string> + <string name="in_developing_desc">ยังไม่รองรับคุณสมบัตินี้ ลองรุ่นถัดไป</string> + <string name="send_receipts_disabled_alert_msg">กลุ่มนี้มีสมาชิกมากกว่า %1$d ไม่มีการส่งใบเสร็จข้อความที่ส่งมา</string> + <string name="you_can_enable_delivery_receipts_later_alert">คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป</string> + <string name="receipts_section_groups">กลุ่มเล็ก (สูงสุด 20 คน)</string> + <string name="sync_connection_force_question">เจรจา enryption ใหม่หรือไม่\?</string> + <string name="v5_2_fix_encryption_descr">แก้ไข encryption หลังจากกู้คืนข้อมูลสำรอง</string> + <string name="error_synchronizing_connection">เกิดข้อผิดพลาดในการซิงโครไนซ์การเชื่อมต่อ</string> + <string name="in_reply_to">ในการตอบกลับถึง</string> + <string name="no_history">ไม่มีประวัติ</string> + <string name="conn_event_ratchet_sync_ok">encryptionใช้ได้</string> </resources> \ No newline at end of file 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 0e7f993721..ab4a04a373 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -2,8 +2,8 @@ <resources> <string name="notifications">Bildirimler</string> <string name="connect_via_link_or_qr">Bağlantı ya da karekod ile bağlan</string> - <string name="create_group">Gizli toplu konuşma oluştur</string> - <string name="create_one_time_link">Tek kullanımlık davet bağlantısı oluştur</string> + <string name="create_group">Gizli grup oluştur</string> + <string name="create_one_time_link">Tek seferlik davet bağlantısı oluştur</string> <string name="your_simplex_contact_address">SimpleX addresin</string> <string name="callstatus_missed">cevapsız çağrı</string> <string name="incoming_video_call">Gelen görüntülü arama</string> @@ -38,7 +38,7 @@ <string name="notifications_mode_service">Sürekli açık</string> <string name="abort_switch_receiving_address_desc">Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır.</string> <string name="send_disappearing_message_1_minute">1 dakika</string> - <string name="clear_chat_warning">Tüm iletiler silinecektir. Bu, geri alınamaz! İletiler, YALNIZCA senin için silinecektir.</string> + <string name="clear_chat_warning">Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir.</string> <string name="smp_servers_add">Sunucu ekle…</string> <string name="database_passphrase_and_export">Veri tabanı ayarları</string> <string name="one_time_link_short">tek kullanımlık bağlantı</string> @@ -48,7 +48,7 @@ <string name="app_version_name">Uygulama sürümü: v%s</string> <string name="icon_descr_audio_call">sesli arama</string> <string name="audio_call_no_encryption">sesli arama (uçtan uca şifreli değil)</string> - <string name="answer_call">Çağrıya cevap ver</string> + <string name="answer_call">Aramaya cevap ver</string> <string name="full_backup">Uygulama veri yedekleme</string> <string name="all_app_data_will_be_cleared">Tüm uygulama verileri silinir.</string> <string name="settings_section_title_app">UYGULAMA</string> @@ -56,27 +56,25 @@ <string name="chat_item_ttl_week">1 hafta</string> <string name="conn_event_ratchet_sync_started">şifreleme kabul ediliyor…</string> <string name="group_member_role_admin">yönetici</string> - <string name="button_add_welcome_message">Karşılama iletisi ekleyin</string> - <string name="users_delete_all_chats_deleted">Tüm konuşmalar ve iletiler silinecektir. Bu, geri alınamaz!</string> - <string name="incognito_random_profile_description">Kişinize rastgele bir profil gönderilecek</string> - <string name="incognito_random_profile_from_contact_description">Bu bağlantıyı aldığınız kişiye rastgele bir profil gönderilecek</string> + <string name="button_add_welcome_message">Karşılama mesajı ekleyin</string> + <string name="users_delete_all_chats_deleted">Tüm konuşmalar ve mesajlar silinecektir. Bu, geri alınamaz!</string> <string name="color_primary">Ana renk</string> <string name="chat_preferences_always">sürekli</string> - <string name="allow_message_reactions">İleti tepkilerine izin ver.</string> + <string name="allow_message_reactions">Mesaj tepkilerine izin ver.</string> <string name="v4_3_improved_server_configuration_desc">Karekodu okutarak sunucular ekle.</string> <string name="v4_6_audio_video_calls">Sesli ve görüntülü aramalar</string> <string name="chat_item_ttl_day">1 gün</string> <string name="chat_item_ttl_month">1 ay</string> <string name="add_address_to_your_profile">Profilinize adres ekleyin, böylece kişileriniz bunu diğer insanlarla paylaşabilir. Profil güncellemesi kişilerinize gönderilecektir.</string> <string name="address_section_title">Adres</string> - <string name="v4_2_group_links_desc">Yöneticiler, toplu konuşmalara katılım bağlantısı oluşturabilirler.</string> + <string name="v4_2_group_links_desc">Yöneticiler, gruplara katılım bağlantısı oluşturabilirler.</string> <string name="all_group_members_will_remain_connected">Konuşma üyelerinin tümü bağlı kalacaktır.</string> <string name="allow_verb">İzin ver</string> <string name="v5_0_app_passcode">Uygulama erişim kodu</string> - <string name="network_session_mode_user_description"><b>Uygulamadaki her konuşma profliniz için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.</string> - <string name="network_session_mode_entity_description"><b>Konuştuğun kişilerin ve toplu konuşma üyelerinin bütünü için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır. + <string name="network_session_mode_user_description"><![CDATA[<b>Uygulamadaki her konuşma profliniz için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır.]]></string> + <string name="network_session_mode_entity_description"><b>Konuştuğun kişilerin ve grup üyelerinin tamamı için</b> ayrı bir TCP bağlantısı (ve SOCKS kimliği) kullanılacaktır. \n<b>Bilgin olsun</b>: Çok sayıda bağlantın varsa pilin ve veri kullanımın önemli ölçüde artabilir ve bazı bağlantılar başarısız olabilir.</string> - <string name="save_and_notify_group_members">Kaydet ve toplu konuşma üyelerine bildir</string> + <string name="save_and_notify_group_members">Kaydet ve grup üyelerini bilgilendir</string> <string name="notifications_mode_off">Uygulama açıkken çalışır</string> <string name="search_verb">Ara</string> <string name="reveal_verb">Göster</string> @@ -96,7 +94,7 @@ <string name="save_and_notify_contacts">Kaydet ve konuştuğun kişilere bildir</string> <string name="save_preferences_question">Tercihleri kaydet\?</string> <string name="save_profile_password">Profil parolasını kaydet</string> - <string name="profile_is_only_shared_with_your_contacts">Profil yalnızca konuştuğun kişilerle paylaşılır.</string> + <string name="profile_is_only_shared_with_your_contacts">Profil sadece konuştuğun kişilerle paylaşılır.</string> <string name="next_generation_of_private_messaging">Gizli iletişimin gelecek kuşağı</string> <string name="icon_descr_audio_off">Ses kapalı</string> <string name="authentication_cancelled">Doğrulama iptal edildi</string> @@ -111,10 +109,10 @@ <string name="database_downgrade">Veri tabanının yapısal gerilemesi</string> <string name="save_archive">Arşivi kaydet</string> <string name="current_version_timestamp">%s (mevcut)</string> - <string name="save_and_update_group_profile">Kaydet ve toplu konuşma profilini güncelle</string> - <string name="save_welcome_message_question">Karşılama iletisini kaydet\?</string> + <string name="save_and_update_group_profile">Kaydet ve grup profilini güncelle</string> + <string name="save_welcome_message_question">Karşılama mesajını kaydet\?</string> <string name="network_options_reset_to_defaults">Varsayılana sıfırla</string> - <string name="save_group_profile">Toplu konuşma profilini kaydet</string> + <string name="save_group_profile">Grup profilini kaydet</string> <string name="network_option_seconds_label">sn</string> <string name="network_options_save">Kaydet</string> <string name="should_be_at_least_one_visible_profile">There should be at least one visible user profile.</string> @@ -144,43 +142,42 @@ <string name="save_and_notify_contact">Kaydet ve kişiyi bilgilendir</string> <string name="save_passphrase_in_keychain">Parolayı, Keystore\'a kaydet.</string> <string name="icon_descr_audio_on">Ses açık</string> - <string name="member_role_will_be_changed_with_notification">Yetki, \"%s\" olarak değiştirelecek. Toplu konuşmadakilerin bütününe bildirilecek.</string> + <string name="member_role_will_be_changed_with_notification">Yetki, \"%s\" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string> <string name="calls_prohibited_with_this_contact">Sesli/görüntülü aramalar yasaktır.</string> <string name="la_auth_failed">Kimlik doğrulama başarısız</string> <string name="icon_descr_address">SimpleX adresi</string> - <string name="moderate_message_will_be_deleted_warning">İleti, tüm üyeler için silinecek.</string> - <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Gizliliğinizi ve güvenliğinizi koruyan iletişim ve uygulama platformu.</string> + <string name="moderate_message_will_be_deleted_warning">Mesaj, tüm üyeler için silinecek.</string> + <string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Gizliliğinizi ve güvenliğinizi koruyan mesajlaşma ve uygulama platformu.</string> <string name="settings_section_title_incognito">Gizlilik kipi</string> - <string name="member_role_will_be_changed_with_invitation">Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davetiye alacak.</string> - <string name="group_unsupported_incognito_main_profile_sent">Gizlilik kipi burada desteklenmiyor. Ana profiliniz, toplu konuşma üyelerine gönderilecek</string> + <string name="member_role_will_be_changed_with_invitation">Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davet alacak.</string> <string name="role_in_group">Yetki</string> - <string name="allow_voice_messages_question">Sesli iletilere izin ver\?</string> + <string name="allow_voice_messages_question">Sesli mesajlara izin ver\?</string> <string name="users_add">Profil ekle</string> - <string name="allow_direct_messages">Üyelere doğrudan ileti gönderilmesine izin ver.</string> - <string name="allow_to_send_disappearing">Kendiliğinden yok olan iletiler göndermeye izin ver.</string> - <string name="allow_to_delete_messages">Gönderilen iletilerin kalıcı olarak silinmesine izin ver.</string> + <string name="allow_direct_messages">Üyelere direkt mesaj gönderilmesine izin ver.</string> + <string name="allow_to_send_disappearing">Kendiliğinden yok olan mesajlar göndermeye izin ver.</string> + <string name="allow_to_delete_messages">Gönderilen mesajların kalıcı olarak silinmesine izin ver.</string> <string name="allow_to_send_files">Dosya ve medya göndermeye izin ver.</string> - <string name="allow_to_send_voice">Sesli ileti göndermeye izin ver.</string> + <string name="allow_to_send_voice">Sesli mesaj göndermeye izin ver.</string> <string name="share_invitation_link">Tek kullanımlık bağlantıyı paylaş</string> <string name="connect_via_invitation_link">Davet bağlantısı ile bağlan\?</string> - <string name="connect_via_group_link">Toplu konuşma bağlantısı ile bağlan\?</string> - <string name="you_will_join_group">Bu bağlantının yönlendirdiği bir toplu konuşmaya katılacak ve üyeleriyle bağlantı kuracaksın.</string> + <string name="connect_via_group_link">Grup bağlantısı ile bağlan\?</string> + <string name="you_will_join_group">Bu bağlantının yönlendirdiği bir gruba katılacak ve üyeleriyle bağlantı kuracaksın.</string> <string name="opening_database">Veri tabanı açılıyor…</string> <string name="server_connecting">bağlanılıyor</string> <string name="moderated_item_description">%s yönetiyor</string> <string name="sending_files_not_yet_supported">dosya gönderme henüz desteklenmiyor</string> - <string name="unknown_message_format">bilinmeyen ileti biçimi</string> - <string name="invalid_message_format">geçersiz ileti biçimi</string> + <string name="unknown_message_format">bilinmeyen mesaj biçimi</string> + <string name="invalid_message_format">geçersiz mesaj biçimi</string> <string name="live">CANLI</string> <string name="invalid_data">geçersiz veri</string> <string name="display_name_connection_established">bağlantı kuruldu</string> <string name="display_name_invited_to_connect">bağlanmaya davet etti</string> <string name="display_name_connecting">bağlanılıyor…</string> - <string name="description_you_shared_one_time_link">tek kullanımlık link paylaştınız</string> - <string name="description_via_group_link">toplu konuşma bağlantısı ile</string> - <string name="description_via_one_time_link">tek kullanımlık bağlantı ile</string> + <string name="description_you_shared_one_time_link">tek seferlik bağlantı paylaştınız</string> + <string name="description_via_group_link">grup bağlantısı ile</string> + <string name="description_via_one_time_link">tek seferlik bağlantı ile</string> <string name="simplex_link_invitation">SimpleX tek kullanımlık bağlantı</string> - <string name="simplex_link_group">SimpleX toplu konuşma bağlantısı</string> + <string name="simplex_link_group">SimpleX grup bağlantısı</string> <string name="simplex_link_mode">SimpleX bağlantıları</string> <string name="simplex_link_mode_description">Açıklama</string> <string name="simplex_link_mode_full">Bütün olan bağlantı</string> @@ -200,18 +197,18 @@ <string name="connection_error">Bağlantı hatası</string> <string name="connection_timeout">Bağlantı süre aşımı</string> <string name="cannot_receive_file">Dosya alınamıyor</string> - <string name="notifications_mode_periodic_desc">Yeni iletilerin gelme durumunu en fazla 1 dakikalığına her 10 dakika denetle.</string> + <string name="notifications_mode_periodic_desc">Her 10 dakikada bir yeni mesajları kontrol eder</string> <string name="reply_verb">Yanıtla</string> <string name="chat_with_developers">Geliştiricilerle konuş</string> <string name="group_connection_pending">bağlanılıyor…</string> <string name="attach">Ekle</string> <string name="icon_descr_server_status_connected">Bağlanıldı</string> - <string name="disappearing_message">Kendiliğinden yok olan ileti</string> - <string name="send_disappearing_message">Kendiliğinden yok olan ileti gönder</string> + <string name="disappearing_message">Kendiliğinden yok olan mesaj</string> + <string name="send_disappearing_message">Kendiliğinden yok olan mesaj gönder</string> <string name="send_disappearing_message_send">Gönder</string> <string name="use_camera_button">Kamera</string> <string name="cancel_verb">İptal et</string> - <string name="icon_descr_cancel_live_message">Canlı iletileri iptal et</string> + <string name="icon_descr_cancel_live_message">Canlı mesajları iptal et</string> <string name="confirm_verb">Onayla</string> <string name="gallery_video_button">Video</string> <string name="reset_verb">Sıfırla</string> @@ -244,7 +241,7 @@ <string name="icon_descr_call_progress">Arama yapılıyor</string> <string name="icon_descr_call_rejected">Geri çevrilmiş çağrı</string> <string name="icon_descr_call_ended">Görüşme bitti.</string> - <string name="alert_text_decryption_error_too_many_skipped">%1$d ileti atlanıldı.</string> + <string name="alert_text_decryption_error_too_many_skipped">%1$d mesaj atlanıldı.</string> <string name="chat_database_deleted">Konuşma veri tabanı silindi</string> <string name="error_with_info">Hata: %s</string> <string name="unknown_error">Bilinmeyen hata</string> @@ -259,26 +256,26 @@ <string name="color_primary_variant">Ek ana renk</string> <string name="chat_preferences_yes">evet</string> <string name="accept_feature">Onayla</string> - <string name="allow_disappearing_messages_only_if">Konuştuğun kişiler, kendiliğinden yok olan iletilere izin veriyorsa sen de ver.</string> - <string name="allow_your_contacts_irreversibly_delete">Konuştuğun kişilerin gönderilen iletileri kalıcı olarak silmesine izin ver.</string> - <string name="prohibit_sending_disappearing_messages">Kendiliğinden yok olan ileti gönderimini engelle.</string> - <string name="allow_irreversible_message_deletion_only_if">Konuştuğun kişi, kalıcı olarak silinebilen iletilere izin veriyorsa sen de ver.</string> - <string name="allow_message_reactions_only_if">Konuştuğun kişi, ileti tepkilerine izin veriyorsa sen de ver.</string> - <string name="only_you_can_send_disappearing">Yalnızca sen kendiliğinden yok olan ileti gönderebilirsin.</string> - <string name="only_your_contact_can_send_disappearing">Yalnızca konuştuğun kişi kendiliğinden yok olan ileti gönderebilir.</string> - <string name="both_you_and_your_contact_can_send_disappearing">Konuştuğun kişi de sen de kendiliğinden yok olan iletileri göndermeye izin veriyorsunuz.</string> - <string name="disappearing_prohibited_in_this_chat">Bu konuşmada kendiliğinden yok olan iletilere izin verilmiyor.</string> + <string name="allow_disappearing_messages_only_if">Konuştuğun kişiler, kendiliğinden yok olan mesajlara izin veriyorsa sen de ver.</string> + <string name="allow_your_contacts_irreversibly_delete">Konuştuğun kişilerin gönderilen mesajları kalıcı olarak silmesine izin ver.</string> + <string name="prohibit_sending_disappearing_messages">Kendiliğinden yok olan measj gönderimini engelle.</string> + <string name="allow_irreversible_message_deletion_only_if">Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver.</string> + <string name="allow_message_reactions_only_if">Konuştuğun kişi, mesaj tepkilerine izin veriyorsa sen de ver.</string> + <string name="only_you_can_send_disappearing">Sadece sen kendiliğinden yok olan mesaj gönderebilirsin.</string> + <string name="only_your_contact_can_send_disappearing">Sadece konuştuğun kişi kendiliğinden yok olan mesaj gönderebilir.</string> + <string name="both_you_and_your_contact_can_send_disappearing">Konuştuğun kişi de sen de kendiliğinden yok olan mesaj gönderebilirsiniz.</string> + <string name="disappearing_prohibited_in_this_chat">Bu konuşmada kendiliğinden yok olan mesajlara izin verilmiyor.</string> <string name="both_you_and_your_contact_can_make_calls">Konuştuğun kişi de sen de arama yapabilirsiniz.</string> - <string name="v4_4_disappearing_messages_desc">Gönderilen iletiler, önceden belirlenmiş bir süre sonra silecektir.</string> + <string name="v4_4_disappearing_messages_desc">Gönderilen mesajlar, önceden belirlenmiş bir süre sonra silecektir.</string> <string name="v5_1_self_destruct_passcode_descr">Kullanıldığında bütün veriler silinir.</string> - <string name="timed_messages">Kendiliğinden yok olan iletiler</string> - <string name="allow_your_contacts_to_send_disappearing_messages">Konuştuğun kişilerin sana, kendiğinden yok olan iletiler göndermesine izin ver.</string> - <string name="disappearing_messages_are_prohibited">Bu toplu konuşmada kendiliğinden yok olan iletilere izin verilmiyor.</string> - <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d ileti deşifrelenemedi.</string> + <string name="timed_messages">Kendiliğinden yok olan mesajlar</string> + <string name="allow_your_contacts_to_send_disappearing_messages">Konuştuğun kişilerin sana, kendiğinden yok olan mesajlar göndermesine izin ver.</string> + <string name="disappearing_messages_are_prohibited">Bu grupta kendiliğinden yok olan mesajlara izin verilmiyor.</string> + <string name="alert_text_decryption_error_n_messages_failed_to_decrypt">%1$d mesaj deşifrelenemedi.</string> <string name="group_info_section_title_num_members">%1$s ÜYE</string> - <string name="integrity_msg_skipped">%1$d atlanılmış ileti</string> + <string name="integrity_msg_skipped">%1$d atlanılmış mesaj</string> <string name="allow_calls_only_if">Konuştuğun kişi, aramalara izin veriyorsa sen de ver.</string> - <string name="allow_your_contacts_adding_message_reactions">Konuştuğun kişilerin iletilerine tepki eklemesine izin ver.</string> + <string name="allow_your_contacts_adding_message_reactions">Konuştuğun kişilerin mesajlarına tepki eklemesine izin ver.</string> <string name="allow_your_contacts_to_call">Konuştuğun kişilerin seni aramasına izin ver.</string> <string name="all_your_contacts_will_remain_connected">Konuştuğun kişilerin tümü bağlı kalacaktır.</string> <string name="icon_descr_cancel_file_preview">Dosya ön izlemesini iptal et</string> @@ -310,14 +307,14 @@ <string name="group_info_member_you">sen: %1$s</string> <string name="group_member_status_removed">çıkarıldı</string> <string name="member_info_section_title_member">ÜYE</string> - <string name="group_members_can_send_disappearing">Toplu konuşma üyeleri kendiliğinden yok olan iletiler gönderebilir.</string> - <string name="prohibit_sending_disappearing">Kendiliğinden yok olan ileti gönderimini engelle.</string> - <string name="allow_voice_messages_only_if">Konuştuğun kişi, sesli iletilere izin veriyorsa sen de ver.</string> - <string name="allow_your_contacts_to_send_voice_messages">Konuştuğun kişilerin sesli ileti göndermesine izin ver.</string> - <string name="both_you_and_your_contact_can_add_message_reactions">Konuştuğun kişi de sen de iletilere tepki ekleyebilirsinsiz.</string> - <string name="both_you_and_your_contact_can_send_voice">Konuştuğun kişi de sen de sesli ileti gönderebilirsiniz.</string> - <string name="v4_4_disappearing_messages">Kendiliğinden yok olan iletiler</string> - <string name="both_you_and_your_contacts_can_delete">Konuştuğun kişi de sen de iletilere kalıcı olarak silebilirsiniz.</string> + <string name="group_members_can_send_disappearing">Grup üyeleri kendiliğinden yok olan mesajlar gönderebilir.</string> + <string name="prohibit_sending_disappearing">Kendiliğinden yok olan mesaj gönderimini engelle.</string> + <string name="allow_voice_messages_only_if">Konuştuğun kişi, sesli mesajlara izin veriyorsa sen de ver.</string> + <string name="allow_your_contacts_to_send_voice_messages">Konuştuğun kişilerin sesli mesaj göndermesine izin ver.</string> + <string name="both_you_and_your_contact_can_add_message_reactions">Konuştuğun kişi de sen de mesajlara tepki ekleyebilirsinsiz.</string> + <string name="both_you_and_your_contact_can_send_voice">Konuştuğun kişi de sen de sesli mesaj gönderebilirsiniz.</string> + <string name="v4_4_disappearing_messages">Kendiliğinden yok olan mesajlar</string> + <string name="both_you_and_your_contacts_can_delete">Konuştuğun kişi de sen de mesajları kalıcı olarak silebilirsiniz.</string> <string name="decryption_error">Deşifreleme hatası</string> <string name="description_via_contact_address_link">kişi adres bağlantısı ile</string> <string name="simplex_link_contact">SimpleX bağlantı adresi</string> @@ -337,7 +334,7 @@ <string name="network_error_desc">Lütfen, %1$s ile ağ bağlantınızı denetleyin ve yeniden deneyin.</string> <string name="error_adding_members">Üye(ler) eklenirken hata oluştu</string> <string name="error_loading_details">Ayrıntıları yüklerken hata oluştu</string> - <string name="error_sending_message">İleti gönderilirken hata oluştu</string> + <string name="error_sending_message">Mesaj gönderilirken hata oluştu</string> <string name="error_creating_address">Adres oluştururken hata oluştu</string> <string name="error_changing_address">Adres değiştirirken hata oluştu</string> <string name="contact_wants_to_connect_via_call">1$s sizinle şu yolla bağlantı kurmak istiyor</string> @@ -366,10 +363,10 @@ <string name="delete_link_question">Bağlantıyı sil\?</string> <string name="ttl_hours">%d saat</string> <string name="v4_6_group_moderation_descr">Yeni yöneticiler artık: -\n- üyelerin iletilerini silebilir. +\n- üyelerin mesajlarını silebilir. \n- üyeleri etkisizleştirebilir (\"gözlemci\" yetkisi)</string> <string name="receipts_section_contacts">Konuşmalar</string> - <string name="create_group_link">Toplu konuşma bağlantısı oluştur</string> + <string name="create_group_link">Grup bağlantısı oluştur</string> <string name="error_accepting_contact_request">Konuşma isteğini onaylarken hata oluştu</string> <string name="error_aborting_address_change">Adres değişikliğini iptal ederken hata oluştu</string> <string name="smp_server_test_create_file">Doysa oluştur</string> @@ -381,8 +378,8 @@ <string name="la_current_app_passcode">Güncel Erişim Kodu</string> <string name="copy_verb">Kopyala</string> <string name="delete_verb">Sil</string> - <string name="delete_message__question">İletiyi sil\?</string> - <string name="delete_member_message__question">Üye iletisini sil\?</string> + <string name="delete_message__question">Mesajı sil\?</string> + <string name="delete_member_message__question">Üye mesajını sil\?</string> <string name="for_me_only">Benim için sil</string> <string name="icon_descr_context">İçerik simgesi</string> <string name="image_decoding_exception_title">Dekodlama hatası</string> @@ -390,8 +387,8 @@ <string name="icon_descr_server_status_error">Hata</string> <string name="send_disappearing_message_custom_time">Kişiselleştirilmiş süre</string> <string name="copied">Panoya kopyalandı</string> - <string name="share_one_time_link">Tek kullanımlık davet bağlantısı oluştur</string> - <string name="desktop_scan_QR_code_from_app_via_scan_QR_code">💻 masaüstü: uygulamadaki <b>Karekodu okut</b> ile karekodu okut</string> + <string name="share_one_time_link">Tek seferlik davet bağlantısı oluştur</string> + <string name="desktop_scan_QR_code_from_app_via_scan_QR_code"><![CDATA[💻 masaüstü: uygulamadaki <b>Karekodu okut</b> ile karekodu okut]]></string> <string name="delete_pending_connection__question">Bekleyen bağlantıları sil\?</string> <string name="delete_contact_menu_action">Sil</string> <string name="delete_group_menu_action">Sil</string> @@ -413,7 +410,7 @@ <string name="decentralized">Özeksiz</string> <string name="confirm_passcode">Erişim kodunu onayla</string> <string name="incorrect_passcode">Yanlış erişim kodu</string> - <string name="new_passcode">Yeni Erişim Kodu</string> + <string name="new_passcode">Yeni Şifre</string> <string name="submit_passcode">Gönder</string> <string name="la_mode_passcode">Erişim kodu</string> <string name="passcode_changed">Erişim kodu değişti!</string> @@ -440,7 +437,7 @@ <string name="delete_archive">Belgeliği sil</string> <string name="delete_chat_archive_question">Konuşma belgeliğini sil\?</string> <string name="rcv_group_event_changed_member_role">%s üyesinin yetkisi %s olarak değiştirildi</string> - <string name="rcv_group_event_group_deleted">silinmiş toplu konuşma</string> + <string name="rcv_group_event_group_deleted">silinmiş grup</string> <string name="snd_group_event_changed_role_for_yourself">kendi yetkini, %s olarak değiştirdin</string> <string name="group_member_role_observer">gözlemci</string> <string name="group_member_status_creator">oluşturan</string> @@ -450,7 +447,7 @@ <string name="info_row_deleted_at">Şu tarihte silindi</string> <string name="share_text_deleted_at">Şu tarihte silindi: %s</string> <string name="item_info_current">(güncel)</string> - <string name="change_member_role_question">Toplu konuşma yetkisini değiştir\?</string> + <string name="change_member_role_question">Grup yetkisini değiştir\?</string> <string name="chat_preferences_default">varsayılan (%s)</string> <string name="v5_1_custom_themes_descr">Renk temalarını kişiselleştir ve paylaş</string> <string name="v5_1_custom_themes">Kişiselleştirilmiş temalar</string> @@ -461,8 +458,8 @@ <string name="delete_files_and_media_for_all_users">Tüm konuşma profilleri için dosyaları sil</string> <string name="current_passphrase">Güncel parola…</string> <string name="database_encrypted">Veri tabanı şifrelendi!</string> - <string name="delete_messages">İletileri sil</string> - <string name="delete_messages_after">Şu süre sonra iletileri sil</string> + <string name="delete_messages">Mesajları sil</string> + <string name="delete_messages_after">Şu süre sonra mesajları sil</string> <string name="total_files_count_and_size">%d tane dosya, toplam boyutu %s</string> <string name="new_passphrase">Yeni parola…</string> <string name="database_will_be_encrypted_and_passphrase_stored">Veri tabanı şifrelenecek ve parola, Keystore\'a kaydedilecek.</string> @@ -476,13 +473,13 @@ <string name="icon_descr_expand_role">Yetki seçimini genişlet</string> <string name="initial_member_role">İlk olarak verilen yetki</string> <string name="new_member_role">Yeni üye yetkisi</string> - <string name="button_delete_group">Toplu konuşmayı sil</string> - <string name="delete_group_question">Toplu konuşmayı sil\?</string> + <string name="button_delete_group">Grubu sil</string> + <string name="delete_group_question">Grubu sil\?</string> <string name="button_create_group_link">Bağlantı oluştur</string> <string name="delete_link">Bağlantıyı sil</string> <string name="info_row_database_id">Veri tabanı kimliği</string> <string name="change_role">Yetkiyi değiştir</string> - <string name="create_secret_group_title">Gizli toplu konuşma oluştur</string> + <string name="create_secret_group_title">Gizli grup oluştur</string> <string name="chat_preferences_no">hayır</string> <string name="delete_chat_profile">Konuşma profilini sil</string> <string name="users_delete_question">Konuşma profilini sil\?</string> @@ -507,13 +504,13 @@ <string name="encrypted_video_call">uçtan uca şifreli görüntülü arama</string> <string name="status_e2e_encrypted">uçtan uca şifreli</string> <string name="allow_accepting_calls_from_lock_screen">Ayarlardan, kilitli ekrandan aramaları izin ver.</string> - <string name="integrity_msg_duplicate">Aynısı bulunan ileti</string> + <string name="integrity_msg_duplicate">Aynısı bulunan mesaj</string> <string name="enable_lock">Kilidi etkinleştir</string> <string name="info_row_disappears_at">Kendiliğinden şu sürede yok olacak</string> <string name="share_text_disappears_at">Kendiliğinden şu sürede yok olacak: %s</string> <string name="feature_enabled">etkinleşti</string> - <string name="direct_messages">Doğrudan iletiler</string> - <string name="no_call_on_lock_screen">Etkinsizleştir</string> + <string name="direct_messages">Direkt mesaj</string> + <string name="no_call_on_lock_screen">Devre dışı bırak</string> <string name="display_name_cannot_contain_whitespace">Görünen ad, boşluk gibi aralıklama türleri içeremez.</string> <string name="display_name">Görünen Ad</string> <string name="display_name__field">Görünen ad:</string> @@ -524,19 +521,19 @@ <string name="edit_verb">Düzelt</string> <string name="network_option_enable_tcp_keep_alive">TCP keep-alive özelliğini etkinleştir</string> <string name="encrypted_database">Şifrelenmiş veri tabanı</string> - <string name="receipts_contacts_disable_for_all">Tüm kişiler için etkinsizleştir.</string> - <string name="receipts_contacts_disable_keep_overrides">Etkisizleştir (ayrıcalıklar kalsın)</string> + <string name="receipts_contacts_disable_for_all">Herkes için devre dışı bırak</string> + <string name="receipts_contacts_disable_keep_overrides">Devre dışı bırak (ayrıcalıklar kalsın)</string> <string name="receipts_contacts_enable_for_all">tüm kişiler için etkinleştir</string> - <string name="enable_automatic_deletion_question">Kendi kendine silinen iletileri etkinleştir\?</string> + <string name="enable_automatic_deletion_question">Kendi kendine silinen mesajları etkinleştir\?</string> <string name="encrypt_database">Şifrele</string> <string name="encrypt_database_question">Veri tabanını şifrele\?</string> <string name="conn_event_ratchet_sync_ok">şifreleme etkin</string> <string name="button_edit_group_profile">Toplu konuşma profilini düzenle</string> - <string name="conn_event_ratchet_sync_agreed">şifreleme kararlaştı</string> - <string name="snd_conn_event_ratchet_sync_agreed">%s üyesi için şifreleme kararlaştı</string> - <string name="conn_level_desc_direct">doğrudan</string> + <string name="conn_event_ratchet_sync_agreed">şifreleme kabul edildi</string> + <string name="snd_conn_event_ratchet_sync_agreed">%s üyesi için şifreleme kabul edildi</string> + <string name="conn_level_desc_direct">direkt</string> <string name="dont_show_again">Yeniden gösterme</string> - <string name="direct_messages_are_prohibited_in_chat">Bu toplu konuşmada üyeler arası doğrudan iletiler yasaklıdır.</string> + <string name="direct_messages_are_prohibited_in_chat">Bu grupta üyeler arası direkt mesajlar yasaklıdır.</string> <string name="feature_enabled_for_contact">konuşulan kişi için etkinleşti</string> <string name="feature_enabled_for_you">senin için etkinleşti</string> <string name="ttl_sec">%d sn</string> @@ -555,14 +552,14 @@ <string name="snd_conn_event_ratchet_sync_allowed">%s üyesi için şifrelemenin yeniden anlaşmasını izin verildi</string> <string name="snd_conn_event_ratchet_sync_required">%s üyesi için şifrelemenin yeniden anlaşması gerekiyor</string> <string name="error_removing_member">Üye çıkarılırken hata oluştu</string> - <string name="enter_welcome_message_optional">Karşılama iletisi gir... (isteğe bağlı)</string> + <string name="enter_welcome_message_optional">Karşılama mesajı gir... (isteğe bağlı)</string> <string name="error_receiving_file">Dosyayı alırken hata oluştu</string> <string name="error_saving_group_profile">Toplu konuşma profili güncellenirken hata oluştu</string> <string name="enter_correct_passphrase">Doğru parolayı gir.</string> <string name="conn_event_ratchet_sync_allowed">şifrelemenin yeniden anlaşmasına izin verildi</string> <string name="snd_conn_event_ratchet_sync_ok">%s üyesi için şifreleme etkin</string> <string name="conn_event_ratchet_sync_required">şifrelemenin yeniden anlaşması gerekiyor</string> - <string name="enter_welcome_message">Karşılama iletisi gir…</string> + <string name="enter_welcome_message">Karşılama mesajı gir…</string> <string name="enter_password_to_show">Aramaya parolayı gir</string> <string name="error_synchronizing_connection">Bağlantı eşlenirken hata oluştu</string> <string name="error_setting_address">Adres belirlenirken hata oluştu</string> @@ -600,16 +597,16 @@ <string name="hidden_profile_password">Gizli profil parolası</string> <string name="how_to_use_markdown">Markdown nasıl kullanılır</string> <string name="how_it_works">Nasıl çalışıyor</string> - <string name="immune_to_spam_and_abuse">Kötüye kullanmaya ve istenmeyen iletilere duyarlı</string> + <string name="immune_to_spam_and_abuse">Kötüye kullanmaya ve istenmeyen mesajlara duyarlı</string> <string name="icon_descr_hang_up">Çağırıyı bitir.</string> <string name="icon_descr_flip_camera">Kameranın karşı yüzüne geç</string> <string name="import_database_question">Konuşma veri tabanını içe aktar\?</string> - <string name="alert_message_group_invitation_expired">Toplu konuşma davetiyesi artık geçerli değil. Gönderici tarafından geçersiz kılındı.</string> - <string name="alert_title_no_group">Toplu konuşma bulunamadı!</string> - <string name="delete_group_for_all_members_cannot_undo_warning">Toplu konuşma, tüm üyeleri için silenecektir. Bu, geri alınamaz bir eylemdir!</string> - <string name="delete_group_for_self_cannot_undo_warning">Toplu konuşma, senin için silinecektir. Bu, geri alınamaz bir eylemdir.</string> + <string name="alert_message_group_invitation_expired">Grup daveti artık geçerli değil. Gönderici tarafından kaldırıldı.</string> + <string name="alert_title_no_group">Grup bulunamadı!</string> + <string name="delete_group_for_all_members_cannot_undo_warning">Grup tüm üyeler için silinecektir - bu geri alınamaz!</string> + <string name="delete_group_for_self_cannot_undo_warning">Grup sizin için silinecektir - bu geri alınamaz!</string> <string name="export_theme">Temayı dışa aktar</string> - <string name="group_preferences">Toplu konuşma tercihleri</string> + <string name="group_preferences">Grup tercihleri</string> <string name="error_saving_ICE_servers">ICE sonucuları kaydedilirken hata oluştu</string> <string name="error_updating_user_privacy">Kullanıcı gizliliği güncellenirken hata oluştu</string> <string name="favorite_chat">Gözde</string> @@ -618,15 +615,15 @@ <string name="v5_2_fix_encryption_descr">Yedekleri geri yükledikten sonra şifrelemeyi onar.</string> <string name="v4_4_french_interface">Fransız arayüzü</string> <string name="v4_6_reduced_battery_usage">Daha da azaltılmış pil kullanımı</string> - <string name="group_members_can_add_message_reactions">Toplu konuşma üyeleri, iletilere tepki ekleyebilir.</string> - <string name="group_profile_is_stored_on_members_devices">Toplu konuşma profili, üyelerinin aygıtlarında barındırılmaktadır, sunucularda değil.</string> + <string name="group_members_can_add_message_reactions">Grup üyeleri, mesajlara tepki ekleyebilir.</string> + <string name="group_profile_is_stored_on_members_devices">Grup profili, üyelerinin aygıtlarında barındırılmaktadır, sunucularda değil.</string> <string name="hide_verb">Gizle</string> <string name="user_hide">Gizle</string> - <string name="notification_display_mode_hidden_desc">Konuşulan kişileri ve iletileri gizle</string> + <string name="notification_display_mode_hidden_desc">Konuşulan kişileri ve mesajları gizle</string> <string name="v4_3_improved_privacy_and_security_desc">Uygulamayı, son kullanılanlar kısmından gizle.</string> <string name="how_simplex_works">SimpleX nasıl çalışıyor</string> - <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel">Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakine karekodunu gösterebilir</b> ya da konuştuğun kişiye bir katılım bağlantısı paylaşabilirsin.</string> - <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link">Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakinin karekodunu okutabilirsin</b> ya da konuştuğun kişi seninle bir katılım bağlantısı paylaşabilir.</string> + <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakine karekodunu gösterebilir</b> ya da konuştuğun kişiye bir katılım bağlantısı paylaşabilirsin.]]></string> + <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Eğer yüz yüze görüşemiyorsanız <b>bir görüntülü aramada karşıdakinin karekodunu okutabilirsin</b> ya da konuştuğun kişi seninle bir katılım bağlantısı paylaşabilir.]]></string> <string name="if_you_cant_meet_in_person">Eğer yüz yüze görüşemiyorsanız bir görüntülü aramada karşıdakine karekodunu gösterebilir ya da konuştuğun kişiye bir katılım bağlantısı paylaşabilirsin.</string> <string name="if_you_choose_to_reject_the_sender_will_not_be_notified">Eğer geri çevirmeyi seçersen göndericiye bildirilmeyecek.</string> <string name="if_you_received_simplex_invitation_link_you_can_open_in_browser">Eğer SimplexX Chat katılım bağlantısı alırsan bu bağlantıyı tarayıcında açabilirsin:</string> @@ -643,13 +640,13 @@ <string name="files_and_media_section">Dosya ve medya</string> <string name="file_with_path">Dosya: %s</string> <string name="incompatible_database_version">Uyumsuz veri tabanı sürümü</string> - <string name="icon_descr_group_inactive">Toplu konuşma aylak</string> - <string name="group_invitation_expired">Toplu konuşma davetiyesinin süresi doldu</string> - <string name="snd_group_event_group_profile_updated">toplu konuşma profili güncellendi</string> + <string name="icon_descr_group_inactive">Grup etkin değil</string> + <string name="group_invitation_expired">Grup davetinin süresi doldu</string> + <string name="snd_group_event_group_profile_updated">Grup profili güncellendi</string> <string name="group_member_status_group_deleted">toplu konuşma silindi</string> <string name="error_updating_link_for_group">Toplu konuşma bağlantısı güncellenirken hata oluştu</string> <string name="section_title_for_console">UÇBİRİM İÇİN</string> - <string name="group_link">Toplu konuşma bağlantısı</string> + <string name="group_link">Grup bağlantısı</string> <string name="info_row_group">Toplu konuşma</string> <string name="conn_level_desc_indirect">dolaylı (%1$s)</string> <string name="fix_connection">Bağlantıyı onar</string> @@ -657,22 +654,400 @@ <string name="fix_connection_confirm">Onar</string> <string name="fix_connection_not_supported_by_contact">Konuştuğunuz kişi, onarımı desteklemiyor.</string> <string name="fix_connection_not_supported_by_group_member">Toplu konuşma üyesi, onarımı desteklemiyor.</string> - <string name="group_display_name_field">Toplu konuşmanın görünen adı:</string> - <string name="group_full_name_field">Toplu konuşmanın tam adı:</string> + <string name="group_display_name_field">Grup görünen adı:</string> + <string name="group_full_name_field">Grup tam adı:</string> <string name="files_and_media">Dosya ve medya</string> - <string name="group_members_can_send_dms">Toplu konuşma üyeleri doğrudan iletiler gönderebilir.</string> - <string name="group_members_can_delete">Toplu konuşma üyeleri, gönderilen iletileri kalıcı olarak silebilir.</string> - <string name="group_members_can_send_voice">Toplu konuşma üyeleri sesli ileti gönderebilirler.</string> + <string name="group_members_can_send_dms">Grup üyeleri direkt mesaj gönderebilir.</string> + <string name="group_members_can_delete">Gru üyeleri, gönderilen mesajları kalıcı olarak silebilir.</string> + <string name="group_members_can_send_voice">Grup üyeleri sesli mesaj gönderebilirler.</string> <string name="files_are_prohibited_in_group">Bu toplu konuşmada, dosya ve medya yasaklanmıştır.</string> - <string name="group_members_can_send_files">Toplu konuşma üyeleri dosya ve medya paylaşabilir.</string> - <string name="v4_2_group_links">Toplu konuşma bağlantıları</string> + <string name="group_members_can_send_files">Grup üyeleri dosya ve medya paylaşabilir.</string> + <string name="v4_2_group_links">Grup bağlantıları</string> <string name="v5_2_disappear_one_message_descr">Konuşmada devre dışı bırakıldığında bile</string> <string name="v5_0_large_files_support_descr">Çabuk ve göndericinin çevrim içi olmasını beklemeden!</string> <string name="v5_2_favourites_filter_descr">Okunmamış ve gözde konuşmaları göster.</string> <string name="v5_1_message_reactions_descr">Sonunda onlara kavuştuk! 🚀</string> <string name="v5_2_favourites_filter">Konuşmaları daha çabuk bul</string> - <string name="v4_6_group_moderation">Toplu konuşma öz denetimi</string> - <string name="v4_6_group_welcome_message">Toplu konuşma karşılama iletisi</string> + <string name="v4_6_group_moderation">Grup yönetimi</string> + <string name="v4_6_group_welcome_message">Grup karşılama mesajı</string> <string name="v4_6_hidden_chat_profiles">Gizli konuşma profilleri</string> <string name="custom_time_unit_hours">Saat</string> + <string name="notifications_mode_off_desc">Sadece uygulama çalışırken bildirim alabileceksiniz, hiçbir arka plan hizmeti başlatılmayacaktır</string> + <string name="notification_preview_new_message">yeni mesaj</string> + <string name="welcome">Hoşgeldin!</string> + <string name="personal_welcome">Hoşgeldin %1$s!</string> + <string name="icon_descr_waiting_for_image">Görsel bekleniyor</string> + <string name="waiting_for_image">Görsel bekleniyor</string> + <string name="icon_descr_waiting_for_video">Video bekleniyor</string> + <string name="waiting_for_video">Video bekleniyor</string> + <string name="waiting_for_file">Dosya bekleniyor</string> + <string name="voice_messages_prohibited">Sesli mesajlar yasaktır!</string> + <string name="contact_wants_to_connect_with_you">sana bağlanmak istiyor!</string> + <string name="you_can_accept_or_reject_connection">İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz.</string> + <string name="network_use_onion_hosts_prefer">Mevcut olduğunda</string> + <string name="auto_accept_contact">Otomatik kabul etme</string> + <string name="we_do_not_store_contacts_or_messages_on_servers">Kişilerinizin veya mesajlarınızın hiçbirini (teslim edildikten sonra) sunucularda saklamıyoruz.</string> + <string name="callstate_waiting_for_answer">yanıt bekleniyor…</string> + <string name="callstate_waiting_for_confirmation">onay bekleniyor…</string> + <string name="onboarding_notifications_mode_off">Uygulama çalışıyorken</string> + <string name="webrtc_ice_servers">WebRTC ICE sunucuları</string> + <string name="auto_accept_images">Görüntüleri otomatik kabul et</string> + <string name="empty_chat_profile_is_created">Verilen adla boş bir sohbet profili oluşturulur ve uygulama her zamanki gibi açılır.</string> + <string name="voice_prohibited_in_this_chat">Bu sohbette sesli mesajlar yasaktır.</string> + <string name="v4_2_auto_accept_contact_requests">Bağlanma isteklerini otomatik kabul et</string> + <string name="database_downgrade_warning">Uyarı: Bazı verileri kaybedebilirsiniz!</string> + <string name="all_your_contacts_will_remain_connected_update_sent">Tüm kişileriniz bağlı kalacaktır. Profil güncellemesi kişilerinize gönderilecektir.</string> + <string name="keychain_is_storing_securely">Android Keystore parolayı güvenli bir şekilde saklamak için kullanılır - bildirim hizmetinin çalışmasını sağlar.</string> + <string name="button_welcome_message">Karşılama mesajı</string> + <string name="group_welcome_title">Karşılama mesajı</string> + <string name="voice_messages_are_prohibited">Bu grupta sesli mesajlar yasaktır.</string> + <string name="whats_new">Neler yeni</string> + <string name="new_in_version">%s sürümünde yeni</string> + <string name="whats_new_read_more">Daha fazla bilgi edinin</string> + <string name="v4_3_voice_messages">Sesli mesajlar</string> + <string name="v5_1_better_messages_descr">- 5 dakikaya kadar sesli mesajlar. +\n- özel mesaj kaybolma süreleri ayarlama +\n- mesag düzenleme geçmişi.</string> + <string name="custom_time_unit_weeks">hafta</string> + <string name="contact_already_exists">Kişi zaten mevcut</string> + <string name="invalid_connection_link">Geçersiz bağlantı linki</string> + <string name="icon_descr_instant_notifications">Anlık bildirimler</string> + <string name="service_notifications">Anlık bildirimler</string> + <string name="service_notifications_disabled">Anlık bildirimler devre dışı!</string> + <string name="turn_off_battery_optimization"><![CDATA[Bunu kullanmak için lütfen bir sonraki iletişim kutusunda SimpleX için <b>pil optimizasyonunu devre dışı bırakın</b>. Aksi takdirde, bildirimler devre dışı bırakılacaktır.]]></string> + <string name="notification_preview_mode_contact">Kişi ismi</string> + <string name="auth_device_authentication_is_disabled_turning_off">Cihaz doğrulaması devre dışı. SimpleX Kilidi Kapatılıyor.</string> + <string name="auth_device_authentication_is_not_enabled_you_can_turn_on_in_settings_once_enabled">Cihaz doğrulaması etkin değil. Cihaz doğrulamasını etkinleştirdikten sonra SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</string> + <string name="in_reply_to">Şuna cevap olarak</string> + <string name="group_preview_join_as">%s olarak katıl</string> + <string name="invalid_contact_link">Geçersiz link!</string> + <string name="invalid_QR_code">Geçersiz QR kodu</string> + <string name="smp_servers_invalid_address">Geçersiz sunucu adresi!</string> + <string name="install_simplex_chat_for_terminal">Terminal için SimpleX Chat\'i yükleyin</string> + <string name="icon_descr_call_connecting">Aramaya bağlanılıyor</string> + <string name="integrity_msg_bad_hash">kötü mesaj hash\'i</string> + <string name="alert_title_msg_bad_hash">Kötü mesaj hash\'i</string> + <string name="alert_text_fragment_encryption_out_of_sync_old_database">Siz veya bağlandığınız kişi eski veritabanı yedeğini kullandığında bu durum ortaya çıkabilir.</string> + <string name="chat_is_stopped_indication">Sohbet durduruldu</string> + <string name="join_group_button">Katıl</string> + <string name="join_group_incognito_button">Gizli katıl</string> + <string name="icon_descr_add_members">Üyeleri davet edin</string> + <string name="contact_preferences">Kişi tercihleri</string> + <string name="contacts_can_mark_messages_for_deletion">Kişiler mesajları silinmek üzere işaretleyebilir; siz onları görüntüleyebileceksiniz.</string> + <string name="v4_3_irreversible_message_deletion">Geri alınamaz mesaj silme</string> + <string name="v4_5_italian_interface">İtalyanca arayüz</string> + <string name="choose_file_title">Dosya seç</string> + <string name="scan_QR_code_to_connect_to_contact_who_shows_QR_code"><![CDATA[<b>QR kodunu tara</b>: size QR kodunu gösteren kişiyle bağlantı kurmak için.]]></string> + <string name="invite_friends">Arkadaşlarınızı davet edin</string> + <string name="bold_text">kalın</string> + <string name="italic_text">İtalik</string> + <string name="onboarding_notifications_mode_subtitle">Daha sonra ayarlardan değiştirebilirsiniz.</string> + <string name="status_contact_has_e2e_encryption">Kişi uçtan uca şifrelemeye sahiptir</string> + <string name="status_contact_has_no_e2e_encryption">Kişi uçtan uca şifrelemeye sahip değildir</string> + <string name="chat_is_stopped">Sohbet durduruldu</string> + <string name="impossible_to_recover_passphrase"><![CDATA[<b>Aklınızda bulunsun</b>: kaybederseniz, parolayı kurtaramaz veya değiştiremezsiniz.]]></string> + <string name="chat_archive_header">Sohbet arşivi</string> + <string name="chat_archive_section">SOHBET ARŞİVİ</string> + <string name="group_invitation_item_description">1$s grubuna davet</string> + <string name="join_group_question">Gruba katıl\?</string> + <string name="rcv_group_event_member_added">1$s davet edildi</string> + <string name="rcv_group_event_invited_via_your_group_link">grup bağlantınız üzerinden davet edildi</string> + <string name="group_member_status_invited">davet edildi</string> + <string name="invite_to_group_button">Gruba davet edin</string> + <string name="invite_prohibited">Kişi davet edilemiyor!</string> + <string name="button_add_members">Üyeleri davet edin</string> + <string name="cant_delete_user_profile">Kullanıcı profili silinemiyor!</string> + <string name="color_background">Arka plan</string> + <string name="message_deletion_prohibited">Bu sohbette geri alınamaz mesaj silme yasaktır.</string> + <string name="delete_contact_question">Kişiyi sil\?</string> + <string name="button_delete_contact">Kişiyi sil</string> + <string name="callstatus_connecting">Aramaya bağlanılıyor…</string> + <string name="delivery_receipts_are_disabled">Gönderildi bilgisi kapalı!</string> + <string name="v5_2_more_things">Birkaç şey daha</string> + <string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Daha fazla pil kullanır</b>! Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.]]></string> + <string name="in_developing_title">Çok yakında!</string> + <string name="delete_contact_all_messages_deleted_cannot_undo_warning">Kişi ve tüm mesajlar silinecektir - bu geri alınamaz!</string> + <string name="alert_title_contact_connection_pending">Kişi henüz bağlanmadı!</string> + <string name="onboarding_notifications_mode_service">Anlık</string> + <string name="v5_1_japanese_portuguese_interface">Japonca ve Portekizce kullanıcı arayüzü</string> + <string name="alert_title_cant_invite_contacts">Kişiler davet edilemiyor!</string> + <string name="alert_title_group_invitation_expired">Davetin süresi dolmuş!</string> + <string name="message_deletion_prohibited_in_chat">Geri alınamaz mesaj silme bu grupta yasaktır</string> + <string name="delivery_receipts_title">Mesaj gönderildi bilgisi!</string> + <string name="joining_group">Gruba katılınıyor</string> + <string name="description_you_shared_one_time_link_incognito">tek seferlik gizli bağlantı paylaştınız</string> + <string name="description_via_group_link_incognito">Grup bağlantısı ile gizli</string> + <string name="description_via_contact_address_link_incognito">Bağlantı linki ile gizli</string> + <string name="description_via_one_time_link_incognito">Tek seferlik bağlantı ile gizli</string> + <string name="smp_server_test_compare_file">Dosyaları karşılaştır</string> + <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b>ayarlardan devre dışı bırakılabilir</b> - uygulama çalışıyorken bildirimler gösterilmeye devam edilecektir.]]></string> + <string name="turning_off_service_and_periodic">Pil optimizasyonu etkin, arka plan hizmeti kapatılacak ve düzenli olarak yeni mesajlar kontrol edilmeyecek . Bunları ayarlardan yeniden etkinleştirebilirsiniz.</string> + <string name="enter_passphrase_notification_title">Parola gerekli</string> + <string name="notification_preview_mode_message">Mesaj metni</string> + <string name="la_authenticate">Doğrula</string> + <string name="auth_confirm_credential">Kimlik bilginizi onaylayın</string> + <string name="auth_unavailable">Doğrulama mevcut değil</string> + <string name="message_delivery_error_title">Mesaj teslim hatası</string> + <string name="delete_message_cannot_be_undone_warning">Mesajlar silinecek - bu geri alınamaz!</string> + <string name="switch_receiving_address_question">Alıcı adresini değiştir\?</string> + <string name="back">Geri</string> + <string name="add_new_contact_to_create_one_time_QR_code"><![CDATA[<b>Yeni kişi ekle</b>: Kişiniz için tek seferlik QR Kodunuzu oluşturmak için.]]></string> + <string name="you_will_be_connected_when_your_connection_request_is_accepted">Bağlantı isteğiniz kabul edildiğinde bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string> + <string name="you_will_be_connected_when_your_contacts_device_is_online">Kişinizin cihazı çevrimiçi olduğunda bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string> + <string name="learn_more">Daha fazla bilgi edinin</string> + <string name="you_wont_lose_your_contacts_if_delete_address">Eğer sonradan bağlantınızı silseniz bile kişilerinizi kaybetmeyeceksiniz.</string> + <string name="smp_servers_your_server">Sunucunuz</string> + <string name="smp_servers_your_server_address">Sunucu adresiniz</string> + <string name="your_SMP_servers">SMP sunucularınız</string> + <string name="your_XFTP_servers">XFTP sunucularınız</string> + <string name="configure_ICE_servers">ICE sunucularını yapılandır</string> + <string name="email_invite_subject">Hadi SimpleX Chat\'te konuşalım</string> + <string name="your_profile_is_stored_on_device_and_shared_only_with_contacts_simplex_cannot_see_it">Profiliniz cihazınızda saklanır ve sadece kişilerinizle paylaşılır. SimpleX sunucuları profilinizi göremez.</string> + <string name="integrity_msg_bad_id">kötü mesaj kimliği</string> + <string name="your_privacy">Gizliliğiniz</string> + <string name="chat_is_running">Sohbet çalışıyor</string> + <string name="you_will_stop_receiving_messages_from_this_group_chat_history_will_be_preserved">Bu gruptan artık mesaj almayacaksınız. Sohbet geçmişi korunacaktır.</string> + <string name="leave_group_question">Gruptan ayrıl\?</string> + <string name="rcv_group_event_member_left">ayrıldı</string> + <string name="available_in_v51">" +\nv5.1\'de mevcut"</string> + <string name="v4_5_transport_isolation_descr">Sohbet profili ile (varsayılan) veya bağlantı ile (BETA).</string> + <string name="image_descr_link_preview">bağlantı önizleme resmi</string> + <string name="your_profile_is_stored_on_your_device">Profiliniz, kişileriniz ve mesajlar cihazınızda saklanır.</string> + <string name="callstatus_ended">arama sona erdi %1$s</string> + <string name="call_on_lock_screen">Kilit ekranında aramalar:</string> + <string name="alert_title_msg_bad_id">Kötü mesaj kimliği</string> + <string name="settings_section_title_messages">MESAJLAR VE DOSYALAR</string> + <string name="change_database_passphrase_question">Veri tabanı parolasını değiştir\?</string> + <string name="restore_passphrase_not_found_desc">Parola Keystore\'da bulunamadı, lütfen manuel olarak girin. Bu, uygulamanın verilerini bir yedekleme aracı kullanarak geri yüklediyseniz olabilir. Eğer durum böyle değilse, lütfen geliştiricilerle iletişime geçin.</string> + <string name="leave_group_button">Ayrıl</string> + <string name="rcv_conn_event_switch_queue_phase_changing">adres değiştiriliyor…</string> + <string name="snd_conn_event_switch_queue_phase_changing">adres değiştiriliyor…</string> + <string name="group_member_status_left">ayrıldı</string> + <string name="button_leave_group">Gruptan ayrıl</string> + <string name="incognito_random_profile">Rastgele profiliniz</string> + <string name="message_reactions_prohibited_in_this_chat">Mesaj tepkileri bu sohbette yasaklıdır</string> + <string name="v5_1_message_reactions">Mesaj tepkileri</string> + <string name="v5_1_better_messages">Daha iyi mesajlar</string> + <string name="custom_time_unit_minutes">dakika</string> + <string name="mark_read">Okundu olarak işaretle</string> + <string name="mark_unread">Okunmadı olarak işaretle</string> + <string name="chat_console">Sohbet konsolu</string> + <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Pil için iyi</b>. Arka plan hizmeti mesajları 10 dakikada bir kontrol eder. Aramaları veya acil mesajları kaçırabilirsiniz.]]></string> + <string name="v4_4_verify_connection_security_desc">Güvenlik kodlarını kişilerinizle karşılaştırın.</string> + <string name="notifications_mode_service_desc">Arka plan hizmeti her zaman çalışır - mesajlar gelir gelmez bildirim gönderilir.</string> + <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b>Pil için en iyisi</b>. Sadece uygulama çalışırken bildirim alırsınız (arka plan hizmeti YOK).]]></string> + <string name="snd_conn_event_switch_queue_phase_changing_for_member">%s için adres değiştiriliyor.</string> + <string name="large_file">Büyük dosya!</string> + <string name="you_will_be_connected_when_group_host_device_is_online">Grup sahibinin cihazı çevrimiçi olduğunda gruba bağlanacaksınız, lütfen bekleyin veya daha sonra kontrol edin!</string> + <string name="delete_message_mark_deleted_warning">Mesaj silinmek için işaretlenecek. Alıcı(lar) bu mesajı açığa görüntüleyebilecek.</string> + <string name="icon_descr_call_missed">Cevapsız çağrı</string> + <string name="receipts_contacts_enable_keep_overrides">Etkinleştir (geçersiz kılmaları koru)</string> + <string name="messages_section_title">Mesajlar</string> + <string name="downgrade_and_open_chat">Sürüm düşür ve sohbeti aç</string> + <string name="you_sent_group_invitation">Grup daveti gönderdiniz</string> + <string name="member_will_be_removed_from_group_cannot_be_undone">Üye gruptan çıkarılacaktır - bu geri alınamaz!</string> + <string name="switch_receiving_address">Alıcı adresini değiştir</string> + <string name="message_reactions">Mesaj tepkileri</string> + <string name="your_preferences">Tercihleriniz</string> + <string name="message_reactions_are_prohibited">Mesaj tepkileri bu grupta yasaklıdır</string> + <string name="connected_to_server_to_receive_messages_from_contact">Bu kişiden mesaj almak için kullanılan sunucuya bağlısınız.</string> + <string name="you_are_already_connected_to_vName_via_this_link">Zaten şuna bağlısınız: %1$s</string> + <string name="la_could_not_be_verified">Doğrulanamadınız; lütfen tekrar deneyin.</string> + <string name="you_can_turn_on_lock">SimpleX Kilidini Ayarlar üzerinden açabilirsiniz.</string> + <string name="group_preview_you_are_invited">gruba davet edildiniz</string> + <string name="you_have_no_chats">Hiç sohbetiniz yok</string> + <string name="you_are_observer">Gözlemcisiniz</string> + <string name="observer_cant_send_message_title">Mesaj gönderemezsiniz</string> + <string name="view_security_code">Güvenlik kodunu görüntüle</string> + <string name="you_need_to_allow_to_send_voice">Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir.</string> + <string name="xftp_servers">XFTP sunucuları</string> + <string name="your_ICE_servers">ICE sunucularınız</string> + <string name="how_to">Nasıl</string> + <string name="your_current_profile">Mevcut profiliniz</string> + <string name="you_control_servers_to_receive_your_contacts_to_send"><![CDATA[Mesajların hangi sunucu(lar)dan <b>alınacağını</b> siz kontrol edersiniz, kişileriniz - onlara mesaj göndermek için kullandığınız sunucular.]]></string> + <string name="video_call_no_encryption">video arama (uçtan uca şifreli değil)</string> + <string name="your_ice_servers">ICE sunucularınız</string> + <string name="icon_descr_video_off">Video kapalı</string> + <string name="icon_descr_video_on">Video açık</string> + <string name="you_have_to_enter_passphrase_every_time">Uygulama her başladığında parola girmeniz gerekir - parola cihazınızda saklanmaz.</string> + <string name="snd_group_event_member_deleted">%1$s kişisini çıkardınız</string> + <string name="snd_conn_event_switch_queue_phase_completed_for_member">%s kişisi için bağlantı değiştirdiniz</string> + <string name="incognito_info_share">Biriyle gizli bir profil paylaştığınızda, bu profil sizi davet ettikleri gruplar için kullanılacaktır.</string> + <string name="v4_2_auto_accept_contact_requests_desc">İsteğe bağlı karşılama mesajı ile.</string> + <string name="your_contacts_will_remain_connected">Kişileriniz bağlı kalacaktır.</string> + <string name="your_contacts_will_see_it">SimpleX\'teki kişileriniz bunu görecektir. +\nBunu Ayarlardan değiştirebilirsiniz.</string> + <string name="you_can_create_it_later">Daha sonra oluşturabilirsiniz</string> + <string name="you_can_use_markdown_to_format_messages__prompt">Mesajları biçimlendirmek için markdown kullanabilirsiniz:</string> + <string name="your_chat_database">Mesaj veri tabanınız</string> + <string name="your_current_chat_database_will_be_deleted_and_replaced_with_the_imported_one">Mevcut sohbet veritabanınız SİLİNECEK ve içe aktarılan veritabanıyla DEĞİŞTİRİLECEKTİR. +\nBu işlem geri alınamaz - profiliniz, kişileriniz, mesajlarınız ve dosyalarınız geri alınamaz şekilde kaybolacaktır.</string> + <string name="youve_accepted_group_invitation_connecting_to_inviting_group_member">Bu gruba katıldınız. Davet eden grup üyesine bağlanılıyor.</string> + <string name="you_are_invited_to_group_join_to_connect_with_group_members">Gruba davetlisiniz. Grup üyeleriyle bağlantı kurmak için katılın.</string> + <string name="alert_title_cant_invite_contacts_descr">Bu grup için gizli bir profil kullanıyorsunuz - ana profilinizi paylaşmayı önlemek için kişileri davet etmeye izin verilmiyor</string> + <string name="snd_group_event_user_left">ayrıldınız</string> + <string name="you_can_share_group_link_anybody_will_be_able_to_connect">Bir bağlantı veya QR kodu paylaşabilirsiniz - bu durumda herkes gruba katılabilir. Daha sonra silseniz bile grubun üyelerini kaybetmezsiniz.</string> + <string name="you_can_share_this_address_with_your_contacts">Bu adresi kişilerinizle paylaşarak onların %s ile bağlantı kurmasını sağlayabilirsiniz.</string> + <string name="chat_preferences_you_allow">İzin veriyorsunuz</string> + <string name="v5_0_large_files_support">1gb\'a kadar videolar ve dosyalar</string> + <string name="icon_descr_video_snd_complete">Video gönderildi</string> + <string name="video_will_be_received_when_contact_is_online">Kişiniz çevrimiçi olduğunda video alınacaktır, lütfen bekleyin veya daha sonra kontrol edin!</string> + <string name="voice_message">Sesli mesaj</string> + <string name="voice_message_with_duration">Sesli mesaj (%1$s)</string> + <string name="voice_message_send_text">Sesli mesaj…</string> + <string name="you_can_share_your_address">Adresinizi bir bağlantı veya QR kodu olarak paylaşabilirsiniz - herkes size bağlanabilir.</string> + <string name="snd_conn_event_switch_queue_phase_completed">bağlantı değiştirdiniz</string> + <string name="you_can_enable_delivery_receipts_later">Daha sonra Ayarlardan etkinleştirebilirsin</string> + <string name="you_can_enable_delivery_receipts_later_alert">Sonrasında uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz.</string> + <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">Bağlantının tamamlanması için kişinizin çevrimiçi olması gerekir. +\nBu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz).</string> + <string name="contact_sent_large_file">Kişiniz desteklenen maksimum boyuttan (%1$s) daha büyük bir dosya gönderdi.</string> + <string name="video_will_be_received_when_contact_completes_uploading">Kişiniz yüklemeyi tamamladığında video alınacaktır.</string> + <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[Ayrıca bağlantıya tıklayarak da bağlanabilirsiniz. Eğer bağlantı tarayıcda açılırsa, <b>mobil uygulamada aç</b> seçeneğine tıklayın.]]></string> + <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[Soru sormak ve güncellemeleri almak için <font color=#0088ff>SimpleX Chat geliştiricilerine bağlanabilirsiniz</font>.]]></string> + <string name="you_can_hide_or_mute_user_profile">Bir kullanıcının profilini gizleyebilir veya sessize alabilirsiniz - menü için basılı tutun.</string> + <string name="you_must_use_the_most_recent_version_of_database">Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz.</string> + <string name="wrong_passphrase">Yanlış veritabanı parolası</string> + <string name="you_rejected_group_invitation">Grup davetini reddettiniz.</string> + <string name="you_are_invited_to_group">Gruba davet edildiniz</string> + <string name="you_joined_this_group">Bu gruba katıldınız</string> + <string name="voice_messages">Sesli mesajlar</string> + <string name="settings_notification_preview_title">Bildirim ön izlemesi</string> + <string name="settings_notifications_mode_title">Bildirim hizmeti</string> + <string name="message_delivery_error_desc">Büyük olasılıkla bu kişi sizinle olan bağlantısını silmiş.</string> + <string name="no_history">Geçmiş yok</string> + <string name="images_limit_desc">Aynı anda sadece 10 görsel gönderilebilir</string> + <string name="videos_limit_desc">Aynı anda sadece 10 video gönderilebilir</string> + <string name="only_owners_can_enable_files_and_media">Dosyaları ve medyayı sadece grup sahipleri etkinleştirebilir.</string> + <string name="only_group_owners_can_enable_voice">Sesli mesajları sadece grup sahipleri etkinleştirebilir.</string> + <string name="ok">TAMAM</string> + <string name="icon_descr_more_button">Daha fazla</string> + <string name="one_time_link">Tek seferlik davet bağlantısı</string> + <string name="network_settings_title">Ağ ayarları</string> + <string name="shutdown_alert_desc">Siz uygulamayı yeniden başlatana kadar bildirimler çalışmayacaktır</string> + <string name="la_mode_off">Kapalı</string> + <string name="self_destruct_new_display_name">Yeni görünen ad:</string> + <string name="chat_item_ttl_none">asla</string> + <string name="color_surface">Menüler & uyarılar</string> + <string name="chat_preferences_off">kapalı</string>` + <string name="v4_5_multiple_chat_profiles">Çoklu sohbet profilleri</string> + <string name="v4_5_reduced_battery_usage_descr">Daha fazla gelişme yakında geliyor!</string> + <string name="custom_time_unit_months">ay</string> + <string name="no_spaces">Boşluk yok!</string> + <string name="new_database_archive">Yeni veri tabanı arşivi</string> + <string name="old_database_archive">Eski veri tabanı arşivi</string> + <string name="no_received_app_files">Alınan veya gönderilen dosya yok</string> + <string name="notifications_will_be_hidden">Bildirimler sadece uygulama durana kadar gönderilecektir!</string> + <string name="user_mute">Sessize al</string> + <string name="v4_6_reduced_battery_usage_descr">Daha fazla gelişme yakında geliyor!</string> + <string name="only_stored_on_members_devices">(sadece grup üyeleri tarafından saklanır)</string> + <string name="markdown_help">markdown yardımı</string> + <string name="no_info_on_delivery">Gönderi hakkında bilgi yok</string> + <string name="no_selected_chat">Seçili sohbet yok</string> + <string name="add_contact">Tek seferlik davet bağlantısı</string> + <string name="receipts_groups_enable_for_all">Tüm gruplar için etkinleştir</string> + <string name="v5_2_more_things_descr">- daha stabil mesaj iletimi. +\n- biraz daha iyi gruplar. +\n- ve daha fazlası!</string> + <string name="only_client_devices_store_contacts_groups_e2e_encrypted_messages"><![CDATA[Sadece istemci cihazlar <b>2 katmanlı uçtan uca şifreleme</b> ile kullanıcı profillerini, kişileri, grupları ve gönderilen mesajları depolar.]]></string> + <string name="only_group_owners_can_change_prefs">Grup tercihlerini sadece grup sahipleri değiştirebilir.</string> + <string name="item_info_no_text">metin yok</string> + <string name="network_status">Ağ durumu</string> + <string name="feature_off">kapalı</string> + <string name="chat_preferences_on">açık</string> + <string name="change_lock_mode">Kilit modunu değiştir</string> + <string name="trying_to_connect_to_server_to_receive_messages">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor.</string> + <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya irtibat kişinizden size başka bir bağlantı göndermesini isteyin.</string> + <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[Gizliliğinizi korumak için, anlık bildirimler yerine <b>SimpleX arka plan hizmeti</b> kullanılır - günde pilin yüzde birkaçını kullanır.]]></string> + <string name="periodic_notifications">Periyodik bildirimler</string> + <string name="periodic_notifications_disabled">Periyodik bildirimler devre dışı</string> + <string name="enter_passphrase_notification_desc">Bildirimleri almak için lütfen veri tabanı parolasını girin</string> + <string name="la_notice_to_protect_your_information_turn_on_simplex_lock_you_will_be_prompted_to_complete_authentication_before_this_feature_is_enabled">Bilgilerinizi korumak için SimpleX Lock özelliğini açın. +\nBu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenecektir.</string> + <string name="la_notice_turn_on">Aç</string> + <string name="la_please_remember_to_store_password">Hatırlayın veya güvenli bir şekilde saklayın - kaybolan bir parolayı kurtarmanın bir yolu yoktur!</string> + <string name="auth_open_chat_console">Sohbet konsolunu aç</string> + <string name="auth_open_chat_profiles">Sohbet profillerini aö</string> + <string name="this_text_is_available_in_settings">Bu metin ayarlarda mevcut</string> + <string name="no_filtered_chats">Filtrelenmiş sohbet yok</string> + <string name="images_limit_title">Çok fazla görsel!</string> + <string name="videos_limit_title">Çok fazla video!</string> + <string name="observer_cant_send_message_desc">Lütfen grup yöneticisiyle iletişime geçin</string> + <string name="icon_descr_server_status_pending">Bekleniyor</string> + <string name="ask_your_contact_to_enable_voice">Lütfen irtibat kişinizden sesli mesaj göndermeyi etkinleştirmesini isteyin.</string> + <string name="live_message">Canlı mesaj!</string> + <string name="to_connect_via_link_title">Link ile bağlanmak için</string> + <string name="this_link_is_not_a_valid_connection_link">Bu geçerli bir bağlantı linki değil</string> + <string name="this_QR_code_is_not_a_link">Bu QR kodu bir bağlantı değil!</string> + <string name="read_more_in_user_guide_with_link"><![CDATA[Daha fazla bilgi için <font color=#0088ff>Kullanıcı Kılavuzu</font>.]]></string> + <string name="paste_button">Yapıştır</string> + <string name="this_string_is_not_a_connection_link">Bu dize bir bağlantı linki değil!</string> + <string name="rate_the_app">Uygulamaya puan verin</string> + <string name="to_reveal_profile_enter_password">Gizli profilinizi ortaya çıkarmak için, Sohbet profilleriniz sayfasındaki arama alanına tam bir parola girin.</string> + <string name="open_simplex_chat_to_accept_call">Aramayı kabul etmek için SimpleX Chat\'i açın</string> + <string name="status_no_e2e_encryption">uçtan uca şifreleme yok</string> + <string name="call_connection_peer_to_peer">eşler arası</string> + <string name="icon_descr_call_pending_sent">Bekleyen arama</string> + <string name="protect_app_screen">Uygulama ekranını koru</string> + <string name="enter_correct_current_passphrase">Lütfen doğru güncel parolayı girin.</string> + <string name="group_welcome_preview">Önizle</string> + <string name="network_option_protocol_timeout">Protokol zaman aşımı</string> + <string name="color_title">Başlık</string> + <string name="prohibit_calls">Sesli/görüntülü aramaları yasakla.</string> + <string name="prohibit_sending_voice">Sesli mesaj göndermeyi yasakla.</string> + <string name="prohibit_message_reactions_group">Mesaj tepkilerini yasakla.</string> + <string name="v4_5_private_filenames_descr">Zaman dilimi, görsel/ses korumak için UTC kullan.</string> + <string name="v4_5_private_filenames">Özel dosya adları</string> + <string name="to_start_a_new_chat_help_header">Yeni bir sohbet başlatmak için</string> + <string name="people_can_connect_only_via_links_you_share">İnsanlar size sadece paylaştığınız bağlantılar üzerinden ulaşabilir.</string> + <string name="privacy_redefined">Gizlilik yeniden tanımlanıyor</string> + <string name="read_more_in_github">GitHub repomuzda daha fazlasını okuyun.</string> + <string name="onboarding_notifications_mode_periodic">Periyodik</string> + <string name="onboarding_notifications_mode_title">Gizli bildirimler</string> + <string name="paste_the_link_you_received">Alınan linki yapıştır</string> + <string name="unknown_database_error_with_info">Bilinmeyen veri tabanı hatası: %s</string> + <string name="open_chat">Sohbeti aç</string> + <string name="restore_database_alert_desc">Veritabanı yedeğini geri yükledikten sonra önceki şifreyi girin. Bu işlem geri alınamaz.</string> + <string name="network_option_ping_interval">PING aralığı</string> + <string name="network_option_protocol_timeout_per_kb">KB başına protokol zaman aşımı</string> + <string name="unhide_chat_profile">Sohbet profilini gizlemeyi kaldır</string> + <string name="unhide_profile">Profili gizlemeyi kaldır</string> + <string name="profile_password">Profil parolası</string> + <string name="prohibit_message_reactions">Mesaj tepkilerini yasakla.</string> + <string name="prohibit_sending_voice_messages">Sesli mesaj göndermeyi yasakla.</string> + <string name="prohibit_message_deletion">Geri alınamaz mesaj silme işlemini yasakla.</string> + <string name="prohibit_direct_messages">Üyelere direkt mesaj göndermeyi yasakla.</string> + <string name="prohibit_sending_files">Dosya ve medya göndermeyi yasakla.</string> + <string name="v4_4_live_messages">Canlı mesajlar</string> + <string name="v5_0_polish_interface">Arayüz geliştirildi</string> + <string name="unfavorite_chat">Favorilerden çıkar</string> + <string name="make_profile_private">Sohbeti gizli yap!</string> + <string name="profile_update_will_be_sent_to_contacts">Profil güncellemesi kişilerinize gönderilecektir.</string> + <string name="read_more_in_github_with_link"><![CDATA[<font color=#0088ff>GitHub repomuzda</font> daha fazlasını okuyun.]]></string> + <string name="alert_text_fragment_please_report_to_developers">Lütfen geliştiricilere bildirin.</string> + <string name="users_delete_with_connections">Profil ve sunucu bağlantıları</string> + <string name="user_unhide">gizlemeyi kaldır</string> + <string name="v4_3_voice_messages_desc">Maksimum 40 saniye, anında alınır.</string> + <string name="icon_descr_record_voice_message">Sesli mesaj kaydet</string> + <string name="toast_permission_denied">İzin Reddedildi!</string> + <string name="to_share_with_your_contact">(irtibat kişinizle paylaşmak için)</string> + <string name="image_descr_profile_image">profil resmi</string> + <string name="icon_descr_profile_image_placeholder">profil fotoğrafı yer tutucusu</string> + <string name="to_verify_compare">Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın).</string> + <string name="messages_section_description">Bu ayar, mevcut sohbet profilinizdeki mesajlar için geçerlidir</string> + <string name="store_passphrase_securely_without_recover">Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz sohbete erişemezsiniz.</string> + <string name="store_passphrase_securely">Lütfen parolayı güvenli bir şekilde saklayın, kaybederseniz sohbete erişemezsiniz.</string> + <string name="alert_message_no_group">Bu grup artık yok.</string> + <string name="v4_5_message_draft">Mesaj taslağı</string> + <string name="v4_6_hidden_chat_profiles_descr">Sohbet profillerini parola ile koru!</string> + <string name="v4_5_reduced_battery_usage">Daha az pil kullanımı</string> + <string name="to_protect_privacy_simplex_has_ids_for_queues">Gizliliği korumak için, diğer tüm platformlar gibi kullanıcı kimliği kullanmak yerine, SimpleX mesaj kuyrukları için kişilerinizin her biri için ayrı tanımlayıcılara sahiptir.</string> + <string name="trying_to_connect_to_server_to_receive_messages_with_error">Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %1$s).</string> + <string name="v4_4_live_messages_desc">Alıcılar güncellemeleri siz yazdıkça görürler.</string> + <string name="auth_log_in_using_credential">Bilgilerinizi kullanarak giriş yapın</string> + <string name="markdown_in_messages">Mesajlarda Markdown</string> + <string name="users_delete_data_only">Sadece yerel profil verisi</string> + <string name="info_row_local_name">Yerel ad</string> + <string name="only_you_can_add_message_reactions">Sadece sen mesaj tepkileri ekleyebilirsin.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 161d641b3b..1c3ec176d4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -41,7 +41,6 @@ <string name="v5_1_self_destruct_passcode_descr">Всі дані стираються при введенні.</string> <string name="v5_0_app_passcode">Пароль додатку</string> <string name="settings_section_title_icon">ЗНАЧОК ДОДАТКУ</string> - <string name="incognito_random_profile_from_contact_description">Випадковий профіль буде надіслано контакту, від якого ви отримали це посилання</string> <string name="allow_disappearing_messages_only_if">Дозволяйте зникати повідомленням, тільки якщо ваш контакт дозволяє це робити.</string> <string name="allow_your_contacts_adding_message_reactions">Дозвольте вашим контактам додавати реакції на повідомлення.</string> <string name="allow_message_reactions_only_if">Дозволяйте реакції на повідомлення, тільки якщо ваш контакт дозволяє їх.</string> @@ -69,12 +68,11 @@ <string name="address_section_title">Адреса</string> <string name="users_add">Додати профіль</string> <string name="color_secondary_variant">Додатковий вторинний</string> - <string name="incognito_random_profile_description">Випадковий профіль буде надіслано на ваш контакт</string> <string name="notifications_mode_service">Завжди увімкнено</string> <string name="notifications_mode_off_desc">Додаток може отримувати сповіщення лише під час роботи, жодні фонові служби не запускаються</string> <string name="chat_preferences_always">завжди</string> <string name="allow_your_contacts_to_call">Дозвольте вашим контактам телефонувати вам.</string> - <string name="network_session_mode_user_description"><![CDATA[Для кожного профілю чату, який ви маєте в додатку</b>, буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис) <b>.]]></string> + <string name="network_session_mode_user_description"><![CDATA[<b>Для кожного профілю чату, який ви маєте в додатку</b>, буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис).]]></string> <string name="appearance_settings">Зовнішній вигляд</string> <string name="app_version_name">Версія програми: v%s</string> <string name="network_session_mode_entity_description"><b>Для кожного контакту і члена групи</b> буде використовуватися окреме TCP-з\'єднання (і SOCKS-обліковий запис). @@ -83,7 +81,7 @@ <string name="back">Назад</string> <string name="bold_text">жирний</string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Корисно для батареї</b>. Фонова служба перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити дзвінки або термінові повідомлення.]]></string> - <string name="settings_audio_video_calls">Аудіо і відео дзвінки</string> + <string name="settings_audio_video_calls">Аудіо та відео виклики</string> <string name="icon_descr_audio_off">Звук вимкнено</string> <string name="auth_unavailable">Автентифікація недоступна</string> <string name="icon_descr_video_asked_to_receive">Попросили отримати відео</string> @@ -226,7 +224,7 @@ <string name="mobile_tap_open_in_mobile_app_then_tap_connect_in_app"><![CDATA[📱 мобільний: торкніться <b>Відкрийте в мобільному додатку</b>, потім торкніться <b>Підключіть</b> в додатку.]]></string> <string name="mute_chat">Вимкнути звук</string> <string name="unmute_chat">Увімкнути звук</string> - <string name="you_invited_your_contact">Ви запросили свого контакта</string> + <string name="you_invited_a_contact">Ви запросили свого контакта</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">Контакт, якому ви надали це посилання, НЕ зможе підключитися!</string> <string name="icon_descr_profile_image_placeholder">заповнювач зображення профілю</string> <string name="image_descr_qr_code">QR-код</string> @@ -258,7 +256,7 @@ <string name="network_use_onion_hosts">Використовуйте хости .onion</string> <string name="network_use_onion_hosts_prefer">При наявності</string> <string name="network_use_onion_hosts_no">Ні</string> - <string name="network_use_onion_hosts_prefer_desc">Використовуватимуться хости-зони, якщо вони доступні.</string> + <string name="network_use_onion_hosts_prefer_desc">Onion хости будуть використовуватися за наявності.</string> <string name="update_network_session_mode_question">Оновити режим транспортної ізоляції\?</string> <string name="core_simplexmq_version">simplexmq: v%s (%2s)</string> <string name="create_address">Створити адресу</string> @@ -492,7 +490,6 @@ <string name="error_changing_role">Помилка, що змінює роль</string> <string name="group_display_name_field">Відображувана назва групи:</string> <string name="group_full_name_field">Повна назва групи:</string> - <string name="group_unsupported_incognito_main_profile_sent">Режим інкогніто тут не підтримується - ваш основний профіль буде надіслано учасникам групи</string> <string name="error_saving_group_profile">Помилка збереження профілю групи</string> <string name="network_options_reset_to_defaults">Скидання до налаштувань за замовчуванням</string> <string name="network_option_seconds_label">сек</string> @@ -627,7 +624,6 @@ <string name="you_will_be_connected_when_group_host_device_is_online">Вас буде підключено до групи, коли пристрій хоста групи буде онлайн, зачекайте або перевірте пізніше!</string> <string name="you_will_be_connected_when_your_connection_request_is_accepted">Ви будете підключені, коли ваш запит на підключення буде прийнято, будь ласка, зачекайте або перевірте пізніше!</string> <string name="share_invitation_link">Поділіться одноразовим посиланням</string> - <string name="your_profile_will_be_sent">Ваш профіль у чаті буде надіслано вашому контакту</string> <string name="learn_more">Детальніше</string> <string name="scan_qr_to_connect_to_contact">Щоб підключитися, ваш контакт може відсканувати QR-код або скористатися посиланням у додатку.</string> <string name="if_you_cant_meet_in_person">Якщо ви не можете зустрітися особисто, покажіть QR-код у відеодзвінку або поділіться посиланням.</string> @@ -648,7 +644,7 @@ <string name="enter_one_ICE_server_per_line">Сервери ICE (по одному на лінію)</string> <string name="error_saving_ICE_servers">Помилка збереження серверів ICE</string> <string name="network_use_onion_hosts_required_desc">Для з\'єднання будуть потрібні хости onion.</string> - <string name="network_use_onion_hosts_prefer_desc_in_alert">Буде використано хости onion за наявності.</string> + <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion хости будуть використовуватися за наявності.</string> <string name="network_use_onion_hosts_required_desc_in_alert">Для з\'єднання будуть потрібні хости onion.</string> <string name="show_developer_options">Показати опції розробника</string> <string name="developer_options">Ідентифікатори бази даних та опція ізоляції транспорту.</string> @@ -815,7 +811,7 @@ <string name="incognito_info_allows">Це дозволяє мати багато анонімних з\'єднань без будь-яких спільних даних між ними в одному профілі чату.</string> <string name="theme_simplex">SimpleX</string> <string name="only_your_contact_can_send_disappearing">Тільки ваш контакт може надсилати зникаючі повідомлення.</string> - <string name="chat_preferences_on">увімкнено</string> + <string name="chat_preferences_on">увімкнути</string> <string name="ttl_hours">%d годин</string> <string name="error_deleting_pending_contact_connection">Помилка видалення очікуваного з\'єднання контакту</string> <string name="error_loading_details">Помилка завантаження деталей</string> @@ -827,7 +823,7 @@ <string name="smp_server_test_secure_queue">Безпечна черга</string> <string name="smp_server_test_delete_queue">Видалити чергу</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Будь ласка, переконайтеся, що ви використали правильне посилання, або попросіть свого контакта надіслати вам інше.</string> - <string name="turn_off_battery_optimization"><![CDATA[Щоб скористатися нею, будь ласка, <b>вимкніть оптимізацію батареї</b> для SimpleX у наступному діалозі. Інакше сповіщення буде вимкнено.]]></string> + <string name="turn_off_battery_optimization"><![CDATA[Щоб використовувати його, <b>дозвольте SimpleX працювати у фоновому режимі</b> у наступному діалоговому вікні. \u0020В іншому випадку сповіщення буде вимкнено.]]></string> <string name="icon_descr_instant_notifications">Миттєві сповіщення</string> <string name="notification_preview_somebody">Контакт приховано:</string> <string name="notification_preview_new_message">нове повідомлення</string> @@ -861,7 +857,7 @@ <string name="smp_servers_use_server">Використовувати сервер</string> <string name="ensure_ICE_server_address_are_correct_format_and_unique">Переконайтеся, що адреси серверів WebRTC ICE мають правильний формат, розділені рядками та не дублюються.</string> <string name="network_disable_socks_info">Якщо ви підтвердите, сервери обміну повідомленнями зможуть бачити вашу IP-адресу, а ваш провайдер - до яких серверів ви підключаєтеся.</string> - <string name="network_use_onion_hosts_no_desc">Оніонні хости не використовуватимуться.</string> + <string name="network_use_onion_hosts_no_desc">Onion хости не використовуватимуться.</string> <string name="network_session_mode_transport_isolation">Транспортна ізоляція</string> <string name="customize_theme_title">Налаштувати тему</string> <string name="share_address_with_contacts_question">Поділіться адресою з контактами\?</string> @@ -958,7 +954,7 @@ <string name="leave_group_button">Залишити</string> <string name="group_member_role_observer">спостерігач</string> <string name="member_info_section_title_member">УЧАСНИК</string> - <string name="incognito_info_protects">Режим інкогніто захищає конфіденційність імені та зображення вашого основного профілю - для кожного нового контакту створюється новий випадковий профіль.</string> + <string name="incognito_info_protects">Режим інкогніто захищає вашу конфіденційність, використовуючи новий випадковий профіль для кожного контакту.</string> <string name="v4_5_reduced_battery_usage_descr">Незабаром буде ще більше покращень!</string> <string name="only_group_owners_can_enable_voice">Тільки власники груп можуть вмикати голосові повідомлення.</string> <string name="reject_contact_button">Відхилити</string> @@ -966,7 +962,7 @@ <string name="image_descr_link_preview">посилання для попереднього перегляду зображення</string> <string name="icon_descr_cancel_link_preview">скасувати попередній перегляд посилання</string> <string name="icon_descr_settings">Налаштування</string> - <string name="network_use_onion_hosts_no_desc_in_alert">Оніонні хости не використовуватимуться.</string> + <string name="network_use_onion_hosts_no_desc_in_alert">Onion хости не використовуватимуться.</string> <string name="icon_descr_call_progress">Виклик у процесі</string> <string name="database_error">Помилка в базі даних</string> <string name="restore_database_alert_confirm">Відновити</string> @@ -1223,7 +1219,6 @@ <string name="network_option_enable_tcp_keep_alive">Увімкнути TCP keep-alive</string> <string name="users_delete_profile_for">Видалити профіль чату для</string> <string name="user_hide">Приховати</string> - <string name="incognito_info_find">Щоб знайти профіль, який використовується для з\'єднання інкогніто, торкніться імені контакту або групи у верхній частині чату.</string> <string name="theme_dark">Темний</string> <string name="language_system">Система</string> <string name="dark_theme">Темна тема</string> @@ -1249,7 +1244,6 @@ <string name="group_member_role_owner">власник</string> <string name="group_member_status_removed">видалено</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[Якщо ви не можете зустрітися особисто, ви можете <b>відсканувати QR-код у відеодзвінку</b>, або ваш контакт може поділитися посиланням на запрошення.]]></string> - <string name="paste_connection_link_below_to_connect">Вставте отримане посилання у поле нижче, щоб зв\'язатися з вашим контактом.</string> <string name="how_to_use_simplex_chat">Як ним користуватися</string> <string name="network_session_mode_entity">Підключення</string> <string name="disable_onion_hosts_when_not_supported"><![CDATA[Встановіть <i>Використовувати .onion хости</i> на Ні, якщо SOCKS проксі не підтримує їх.]]></string> @@ -1266,4 +1260,118 @@ <string name="ttl_sec">%d сек</string> <string name="search_verb">Пошук</string> <string name="la_mode_off">Вимкнено</string> + <string name="receipts_contacts_override_disabled">Відправлення підтвердження доставлення вимкнено для контакту %d</string> + <string name="receipts_contacts_disable_keep_overrides">Вимкнути (зберегти перевизначення)</string> + <string name="receipts_contacts_enable_for_all">Увімкнути для всіх</string> + <string name="conn_event_ratchet_sync_ok">шифрування ok</string> + <string name="conn_event_ratchet_sync_started">узгодження шифрування…</string> + <string name="snd_conn_event_ratchet_sync_started">узгодження шифрування для %s…</string> + <string name="sender_at_ts">%s в %s</string> + <string name="sending_delivery_receipts_will_be_enabled">Для всіх контактів буде ввімкнено відправку підтвердження доставки.</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">Відправлення підтвердження доставлення буде ввімкнено для всіх контактів у всіх видимих профілях чату.</string> + <string name="you_can_enable_delivery_receipts_later">Ви можете увімкнути пізніше в Налаштуваннях</string> + <string name="dont_enable_receipts">Не вмикати</string> + <string name="error_enabling_delivery_receipts">Помилка активації підтвердження доставлення!</string> + <string name="error_aborting_address_change">Помилка переривання зміни адреси</string> + <string name="abort_switch_receiving_address">Скасувати зміну адреси</string> + <string name="allow_to_send_files">Дозволяє надсилати файли та медіа.</string> + <string name="files_are_prohibited_in_group">Файли та медіа в цій групі заборонені.</string> + <string name="connect_via_link_incognito">Підключайтеся інкогніто</string> + <string name="connect_use_current_profile">Використовувати поточний профіль</string> + <string name="turn_off_battery_optimization_button">Дозвольте</string> + <string name="disable_notifications_button">Вимкнути сповіщення</string> + <string name="turn_off_system_restriction_button">Відкрийте налаштування програми</string> + <string name="system_restricted_background_desc">SimpleX не може працювати у фоновому режимі. Ви будете отримувати сповіщення лише під час роботи програми.</string> + <string name="system_restricted_background_in_call_title">Ніяких фонових дзвінків</string> + <string name="system_restricted_background_in_call_desc">Додаток можна закрити після 1 хвилини роботи у фоновому режимі.</string> + <string name="in_reply_to">У відповідь на</string> + <string name="no_history">Немає історії</string> + <string name="files_and_media_prohibited">Файли та медіа заборонені!</string> + <string name="connect__your_profile_will_be_shared">Ваш профіль %1$s буде опублікований.</string> + <string name="receipts_groups_disable_for_all">Вимкнути для всіх груп</string> + <string name="settings_section_title_delivery_receipts">НАДІШЛІТЬ ПІДТВЕРДЖЕННЯ ДОСТАВКИ НА АДРЕСУ</string> + <string name="connect_via_member_address_alert_title">Підключатися напряму\?</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="connect_via_member_address_alert_desc">Запит на підключення буде надіслано цьому учаснику групи.</string> + <string name="fix_connection_confirm">Виправлення</string> + <string name="files_and_media">Файли та медіа</string> + <string name="v5_2_favourites_filter_descr">Фільтруйте непрочитані та улюблені чати.</string> + <string name="v5_2_favourites_filter">Швидше знаходьте чати</string> + <string name="v5_2_disappear_one_message">Зробити так, щоб одне повідомлення зникло</string> + <string name="no_selected_chat">Немає вибраного чату</string> + <string name="abort_switch_receiving_address_confirm">Скасувати</string> + <string name="choose_file_title">Виберіть файл</string> + <string name="receipts_section_contacts">Контакти</string> + <string name="receipts_section_description">Ці налаштування стосуються вашого поточного профілю</string> + <string name="receipts_contacts_title_disable">Вимкнути підтвердження\?</string> + <string name="receipts_contacts_title_enable">Активувати підтвердження\?</string> + <string name="conn_event_ratchet_sync_allowed">переузгодження шифрування дозволено</string> + <string name="snd_conn_event_ratchet_sync_agreed">узгоджене шифрування для %s</string> + <string name="snd_conn_event_ratchet_sync_allowed">переузгодження шифрування дозволено для %s</string> + <string name="snd_conn_event_ratchet_sync_required">потрібно переузгодження шифрування для %s</string> + <string name="rcv_conn_event_verification_code_reset">змінено код безпеки</string> + <string name="fix_connection_not_supported_by_contact">Виправлення не підтримується контактом</string> + <string name="send_receipts">Надсилати звіти про доставку</string> + <string name="prohibit_sending_files">Заборонити надсилання файлів і медіа.</string> + <string name="v5_2_fix_encryption_descr">Виправити шифрування після відновлення резервних копій.</string> + <string name="v5_2_fix_encryption">Зберігайте свої зв\'язки</string> + <string name="v5_2_more_things_descr">- стабільніше доставлення повідомлень. +\n- трохи кращі групи. +\n- і багато іншого!</string> + <string name="in_developing_title">Вже скоро!</string> + <string name="delivery">Доставка</string> + <string name="no_info_on_delivery">Немає інформації про доставлення</string> + <string name="no_filtered_chats">Немає фільтрованих чатів</string> + <string name="sync_connection_force_confirm">Переузгодьте</string> + <string name="unfavorite_chat">Неулюблене</string> + <string name="receipts_contacts_disable_for_all">Вимкнути для всіх</string> + <string name="receipts_contacts_enable_keep_overrides">Увімкнути (зберегти перевизначення)</string> + <string name="receipts_contacts_override_enabled">Підтвердження надсилання ввімкнено для контакту %d</string> + <string name="receipts_section_groups">Невеликі групи (максимум 20 осіб)</string> + <string name="receipts_groups_title_disable">Вимкнути підтвердження доставлення для груп\?</string> + <string name="receipts_groups_title_enable">Активувати підтвердження доставлення для груп\?</string> + <string name="receipts_groups_override_enabled">Надсилання підтвердження доставлення дозволено для груп %d</string> + <string name="receipts_groups_disable_keep_overrides">Вимкнути (зберегти групові перевизначення)</string> + <string name="receipts_groups_enable_for_all">Увімкнути для всіх груп</string> + <string name="receipts_groups_enable_keep_overrides">Увімкнути (зберегти перевизначення групи)</string> + <string name="conn_event_ratchet_sync_required">потрібне повторне узгодження шифрування</string> + <string name="send_receipts_disabled_alert_title">Підтвердження доставки вимкнено</string> + <string name="send_receipts_disabled">вимкнено</string> + <string name="send_receipts_disabled_alert_msg">У цій групі більше %1$d учасників, підтвердження доставлення не буде надіслано.</string> + <string name="in_developing_desc">Ця функція поки що не підтримується. Спробуйте наступну версію.</string> + <string name="fix_connection">Виправити з\'єднання</string> + <string name="fix_connection_question">Виправити з\'єднання\?</string> + <string name="renegotiate_encryption">Переузгодьте шифрування</string> + <string name="network_option_protocol_timeout_per_kb">Тайм-аут протоколу на КБ</string> + <string name="conn_event_ratchet_sync_agreed">узгоджено шифрування</string> + <string name="rcv_group_event_2_members_connected">%s і %s під\'єднано</string> + <string name="fix_connection_not_supported_by_group_member">Виправлення не підтримується учасником групи</string> + <string name="v5_2_message_delivery_receipts">Підтвердження доставлення повідомлення!</string> + <string name="v5_2_message_delivery_receipts_descr">Другу галочку ми пропустили! ✅</string> + <string name="favorite_chat">Улюблений</string> + <string name="v5_2_more_things">Ще кілька моментів</string> + <string name="connect__a_new_random_profile_will_be_shared">Буде створено новий випадковий профіль.</string> + <string name="delivery_receipts_title">Підтвердження доставлення!</string> + <string name="delivery_receipts_are_disabled">Підтвердження доставлення вимкнено!</string> + <string name="enable_receipts_all">Увімкнути</string> + <string name="snd_conn_event_ratchet_sync_ok">шифрування ok для %s</string> + <string name="v5_2_disappear_one_message_descr">Навіть коли вимкнений у розмові.</string> + <string name="privacy_message_draft">Чернетка повідомлення</string> + <string name="only_owners_can_enable_files_and_media">Тільки власники груп можуть вмикати файли та медіа.</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">Вставте отримане посилання для зв\'язку з вашим контактом…</string> + <string name="receipts_groups_override_disabled">Відправлення підтвердження доставки вимкнено для груп %d</string> + <string name="rcv_group_event_3_members_connected">%s, %s і %s підключено</string> + <string name="receipts_section_description_1">Їх можна перевизначити в налаштуваннях контактів і груп.</string> + <string name="system_restricted_background_warn"><![CDATA[Щоб увімкнути сповіщення, виберіть <b>Використання акумулятора програми</b> / <b>Необмежено</b> в налаштуваннях програми.]]></string> + <string name="system_restricted_background_in_call_warn"><![CDATA[Щоб здійснювати дзвінки у фоновому режимі, виберіть <b>Використання акумулятора програми</b> / <b>Без обмежень</b> у налаштуваннях програми.]]></string> + <string name="connect_use_new_incognito_profile">Використовуйте новий профіль інкогніто</string> + <string name="you_can_enable_delivery_receipts_later_alert">Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми.</string> + <string name="rcv_group_event_n_members_connected">%s, %s та %d інших учасників під\'єднано</string> + <string name="privacy_show_last_messages">Показати останні повідомлення</string> + <string name="error_synchronizing_connection">Помилка синхронізації з\'єднання</string> + <string name="abort_switch_receiving_address_question">Скасувати зміну адреси\?</string> + <string name="abort_switch_receiving_address_desc">Зміна адреси буде скасована. Буде використано стару адресу отримання.</string> + <string name="sync_connection_force_question">Переузгодьте шифрування\?</string> + <string name="sync_connection_force_desc">Шифрування працює і нова угода про шифрування не потрібна. Це може призвести до помилок з\'єднання!</string> + <string name="group_members_can_send_files">Учасники групи можуть надсилати файли та медіа.</string> </resources> \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 21f154caac..bf0fe570cc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -67,7 +67,7 @@ <string name="connect_via_group_link">通过群组链接连接?</string> <string name="connect_via_link_or_qr">通过群组链接/二维码连接</string> <string name="always_use_relay">总是通过中继连接</string> - <string name="allow_your_contacts_irreversibly_delete">允许您的联系人不可撤回地删除已发送消息。</string> + <string name="allow_your_contacts_irreversibly_delete">允许您的联系人永久删除已发送消息。</string> <string name="chat_preferences_contact_allows">联系人允许</string> <string name="allow_voice_messages_only_if">仅有您的联系人许可后才允许语音消息。</string> <string name="group_info_member_you">您: %1$s</string> @@ -98,17 +98,15 @@ <string name="app_version_title">应用程序版本</string> <string name="full_backup">应用程序数据备份</string> <string name="settings_section_title_icon">应用程序图标</string> - <string name="incognito_random_profile_from_contact_description">一个随机个人资料将被发送至给予您链接的联系人那里</string> - <string name="app_version_name">应用程序版本:v%s</string> + <string name="app_version_name">应用版本:v%s</string> <string name="notifications_mode_off_desc">应用程序仅在运行时可以接受通知,没有后台服务会被启动</string> - <string name="incognito_random_profile_description">一个随机资料将发送给您的联系人</string> <string name="auth_unavailable">身份验证不可用</string> <string name="auto_accept_images">自动接受图像</string> <string name="attach">附件</string> <string name="icon_descr_audio_call">语音通话</string> <string name="audio_call_no_encryption">语音通话(非端到端加密)</string> <string name="v4_2_auto_accept_contact_requests">自动接受联系人请求</string> - <string name="integrity_msg_bad_hash">错误消息散列</string> + <string name="integrity_msg_bad_hash">消息散列值错误</string> <string name="integrity_msg_bad_id">错误消息 ID</string> <string name="settings_audio_video_calls">语音和视频通话</string> <string name="turning_off_service_and_periodic">启用电池优化,关闭了后台服务和对新消息的定期请求。您可以在设置里重新启用它们。</string> @@ -124,7 +122,7 @@ <string name="onboarding_notifications_mode_off_desc"><![CDATA[<b> 最长续航 </b>。您只会在应用程序运行时收到通知(无后台服务)。]]></string> <string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b> 较长续航 </b>。后台服务每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]></string> <string name="bold_text">加粗</string> - <string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以不可逆转地删除已发送的消息。</string> + <string name="both_you_and_your_contacts_can_delete">您和您的联系人都可以永久删除已发送的消息。</string> <string name="both_you_and_your_contact_can_send_disappearing">您和您的联系人都可以发送限时消息。</string> <string name="both_you_and_your_contact_can_send_voice">您和您的联系人都可以发送语音消息。</string> <string name="it_can_disabled_via_settings_notifications_still_shown"><![CDATA[<b> 可以在设置里禁用它 </b> - 应用程序运行时仍会显示通知。]]></string> @@ -184,7 +182,6 @@ <string name="joining_group">加入群组中</string> <string name="join_group_incognito_button">加入隐身聊天</string> <string name="settings_section_title_incognito">隐身模式</string> - <string name="group_unsupported_incognito_main_profile_sent">此处不支持隐身模式——您的主要个人资料将发送给群组成员</string> <string name="tap_to_start_new_chat">点击开始一个新聊天</string> <string name="incognito_random_profile">您的随机资料</string> <string name="description_via_contact_address_link_incognito">通过联系地址链接隐身</string> @@ -193,16 +190,14 @@ <string name="group_invitation_tap_to_join_incognito">点击以加入隐身聊天</string> <string name="group_main_profile_sent">您的聊天资料将被发送给群组成员</string> <string name="invite_prohibited_description">您正在尝试邀请与您共享隐身个人资料的联系人加入您使用主要个人资料的群组</string> - <string name="incognito_info_protects">隐身模式可以保护你的主要个人资料名称和图像的隐私——对于每个新的联系人,都会创建一个新的随机个人资料。</string> + <string name="incognito_info_protects">隐身模式通过为每个联系人使用新的随机配置文件来保护您的隐私。</string> <string name="alert_title_cant_invite_contacts_descr">您正在为该群组使用隐身个人资料——为防止共享您的主要个人资料,不允许邀请联系人</string> - <string name="your_profile_will_be_sent">您的聊天资料将被发送给您的联系人</string> <string name="description_via_one_time_link_incognito">通过一次性链接隐身</string> <string name="only_group_owners_can_enable_voice">只有群主可以启用语音信息。</string> <string name="your_privacy">您的隐私设置</string> <string name="privacy_and_security">隐私和安全</string> <string name="smp_servers_save">保存服务器</string> <string name="incognito_info_allows">它允许在一个聊天资料中有多个匿名连接,而它们之间没有任何共享数据。</string> - <string name="incognito_info_find">要查找用于隐身聊天连接的资料,点击聊天顶部的联系人或群组名。</string> <string name="incognito_info_share">当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。</string> <string name="v4_3_improved_server_configuration">改进的服务器配置</string> <string name="icon_descr_email">电邮</string> @@ -290,7 +285,7 @@ <string name="incoming_video_call">视频通话来电</string> <string name="no_call_on_lock_screen">禁用</string> <string name="status_e2e_encrypted">端到端加密</string> - <string name="status_contact_has_e2e_encryption">联系人具有端到端加密</string> + <string name="status_contact_has_e2e_encryption">联系人已开启端到端加密</string> <string name="allow_accepting_calls_from_lock_screen">通过设置启用在锁定屏幕上通话。</string> <string name="icon_descr_call_connecting">连接通话中</string> <string name="status_contact_has_no_e2e_encryption">联系人没有端到端加密</string> @@ -536,7 +531,7 @@ <string name="enter_passphrase_notification_desc">要接收通知,请输入数据库密码</string> <string name="to_preserve_privacy_simplex_has_background_service_instead_of_push_notifications_it_uses_a_few_pc_battery"><![CDATA[为了保护您的隐私,该应用程序没有推送通知,而是具有 <b>SimpleX 后台服务 </b>——它每天使用百分之几的电池。]]></string> <string name="your_settings">您的设置</string> - <string name="turn_off_battery_optimization"><![CDATA[为了使用它,请 <b>禁用电池优化</b>为SimpleX在下一个对话框。否则通知将被禁用。]]></string> + <string name="turn_off_battery_optimization"><![CDATA[要使用它,请在下一个对话框中<b>允许 SimpleX 在后台运行</b>。 否则,通知将被禁用。]]></string> <string name="settings_notification_preview_title">通知预览</string> <string name="enter_passphrase_notification_title">需要密码</string> <string name="periodic_notifications_disabled">定期通知被禁用!</string> @@ -674,7 +669,8 @@ <string name="no_contacts_selected">未选择联系人</string> <string name="one_time_link">一次性邀请链接</string> <string name="feature_off">关闭</string> - <string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。</string> + <string name="network_use_onion_hosts_required_desc">连接需要 Onion 主机。 +\n请注意:如果没有 .onion 地址,您将无法连接到服务器。</string> <string name="network_use_onion_hosts_prefer_desc_in_alert">Onion 主机将在可用时使用。</string> <string name="chat_item_ttl_none">从不</string> <string name="feature_offered_item">已提供 %s</string> @@ -800,7 +796,6 @@ <string name="v4_5_private_filenames">私密文件名</string> <string name="v4_4_french_interface_descr">感谢用户——通过 Weblate 做出贡献!</string> <string name="v4_5_transport_isolation">传输隔离</string> - <string name="paste_connection_link_below_to_connect">将您收到的链接粘贴到下面的框中以与您的联系人联系。</string> <string name="share_invitation_link">分享一次性链接</string> <string name="this_string_is_not_a_connection_link">此字符串不是连接链接!</string> <string name="send_us_an_email">给我们发电子邮件</string> @@ -902,7 +897,7 @@ <string name="voice_messages_prohibited">语音消息禁止发送!</string> <string name="you_need_to_allow_to_send_voice">您需要允许您的联系人发送语音消息才能发送它们。</string> <string name="scan_QR_code">扫描二维码</string> - <string name="you_invited_your_contact">您邀请了您的联系人</string> + <string name="you_invited_a_contact">您邀请了您的联系人</string> <string name="contact_wants_to_connect_with_you">想要与您连接!</string> <string name="alert_text_connection_pending_they_need_to_be_online_can_delete_and_retry">您的联系人需要在线才能完成连接。 \n您可以取消此连接并删除联系人(稍后尝试使用新链接)。</string> @@ -1113,7 +1108,7 @@ <string name="revoke_file__confirm">撤销</string> <string name="audio_video_calls">音频/视频通话</string> <string name="available_in_v51">" -\n在 v5.1 中可用"</string> +\n在 v5.1 版本中可用"</string> <string name="v5_0_app_passcode">应用程序密码</string> <string name="v5_0_polish_interface">波兰语界面</string> <string name="v5_0_polish_interface_descr">感谢用户——通过 Weblate 做出贡献!</string> @@ -1125,7 +1120,7 @@ <string name="only_you_can_make_calls">只有您可以拨打电话。</string> <string name="only_your_contact_can_make_calls">只有您的联系人可以拨打电话。</string> <string name="allow_your_contacts_to_call">允许您的联系人与您进行语音通话。</string> - <string name="allow_calls_only_if">仅当您的联系人许可时才允许呼叫。</string> + <string name="allow_calls_only_if">仅当您的联系人允许时才允许呼叫。</string> <string name="calls_prohibited_with_this_contact">禁止音频/视频通话。</string> <string name="send_disappearing_message_1_minute">1分钟</string> <string name="one_time_link_short">一次性链接</string> @@ -1284,4 +1279,128 @@ <string name="search_verb">搜索</string> <string name="la_mode_off">已关闭</string> <string name="network_option_protocol_timeout_per_kb">协议超时每 KB</string> + <string name="v5_2_favourites_filter">更快的发起聊天</string> + <string name="v5_2_fix_encryption">保持连接</string> + <string name="v5_2_more_things">还有更多</string> + <string name="v5_2_disappear_one_message">移除一个消息</string> + <string name="v5_2_message_delivery_receipts">消息成功送达!</string> + <string name="choose_file_title">选择一个文件</string> + <string name="receipts_section_contacts">联系人</string> + <string name="conn_event_ratchet_sync_started">协调加密中…</string> + <string name="v5_2_more_things_descr">- 更稳定的消息送达. +\n- 更好的群组. +\n- 还有更多!</string> + <string name="connect__a_new_random_profile_will_be_shared">一个新的随机个人档案将被分享。</string> + <string name="snd_conn_event_ratchet_sync_started">与 %s 协调加密中…</string> + <string name="in_developing_desc">该功能还没支持。请尝试下一个版本。</string> + <string name="turn_off_battery_optimization_button">允许</string> + <string name="connect_via_link_incognito">隐身连接</string> + <string name="connect_via_member_address_alert_title">确认发起私聊?</string> + <string name="delivery">发送</string> + <string name="recipient_colon_delivery_status">%s: %s</string> + <string name="in_developing_title">敬请期待!</string> + <string name="database_will_be_encrypted_and_passphrase_stored_in_settings">数据库将被加密,密码将存储在设置中。</string> + <string name="you_can_enable_delivery_receipts_later">您可以稍后在“设置”中启用它</string> + <string name="receipts_groups_disable_for_all">对所有群组关闭</string> + <string name="no_info_on_delivery">无送货信息</string> + <string name="connect__your_profile_will_be_shared">您的个人资料 %1$s 将被共享。</string> + <string name="sending_delivery_receipts_will_be_enabled">将为所有联系人启用送达回执功能。</string> + <string name="turn_off_system_restriction_button">打开应用程序设置</string> + <string name="receipts_groups_enable_for_all">为所有组启用</string> + <string name="rcv_group_event_n_members_connected">%s、%s 和 %d 其他成员已连接</string> + <string name="receipts_contacts_override_enabled">已为 %d 联系人启用送达回执功能</string> + <string name="snd_conn_event_ratchet_sync_agreed">已同意 %s 的加密</string> + <string name="conn_event_ratchet_sync_allowed">允许重新协商加密</string> + <string name="delivery_receipts_title">送达回执!</string> + <string name="connect_use_new_incognito_profile">使用新的隐身个人资料</string> + <string name="v5_2_favourites_filter_descr">过滤未读和收藏的聊天记录。</string> + <string name="rcv_conn_event_verification_code_reset">已更改安全密码</string> + <string name="sending_delivery_receipts_will_be_enabled_all_profiles">将为所有可见聊天配置文件中的所有联系人启用送达回执功能。</string> + <string name="system_restricted_background_in_call_warn"><![CDATA[要在后台拨打电话,请在应用设置中选择<b>应用电池使用情况</b> / <b>无限制</b>。]]></string> + <string name="sender_at_ts">%s 在 %s</string> + <string name="receipts_contacts_title_disable">禁用回执?</string> + <string name="sync_connection_force_question">重新协商加密?</string> + <string name="receipts_section_description_1">可以在联系人和群组设置中覆盖它们。</string> + <string name="receipts_contacts_disable_for_all">对所有联系人关闭</string> + <string name="you_can_change_it_later">随机密码以明文形式存储在设置中。 +\n您可以稍后更改。</string> + <string name="receipts_groups_override_disabled">已禁用 %d 组的送达回执功能</string> + <string name="snd_conn_event_ratchet_sync_required">需要为 %s 重新协商加密</string> + <string name="system_restricted_background_desc">SimpleX 无法在后台运行。只有在应用程序运行时,您才会收到通知。</string> + <string name="receipts_contacts_enable_keep_overrides">启用(保留覆盖)</string> + <string name="database_encryption_will_be_updated_in_settings">即将更新数据库加密密码并将其存储在设置中。</string> + <string name="connect_use_current_profile">使用当前配置文件</string> + <string name="remove_passphrase_from_settings">从设置中删除密码?</string> + <string name="conn_event_ratchet_sync_agreed">同意加密</string> + <string name="receipts_contacts_title_enable">启用回执?</string> + <string name="system_restricted_background_in_call_desc">程序在后台运行 1 分钟后可能会关闭。</string> + <string name="privacy_message_draft">留言草稿</string> + <string name="v5_2_disappear_one_message_descr">即使在对话中禁用。</string> + <string name="use_random_passphrase">使用随机密码</string> + <string name="system_restricted_background_in_call_title">无后台通话</string> + <string name="you_can_enable_delivery_receipts_later_alert">您可以稍后通过应用程序隐私和安全设置启用它们。</string> + <string name="save_passphrase_in_settings">在设置中保存密码</string> + <string name="enable_receipts_all">启用</string> + <string name="send_receipts_disabled_alert_msg">该群组成员超过 %1$d ,未发送送达回执。</string> + <string name="fix_connection_question">修复连接?</string> + <string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string> + <string name="setup_database_passphrase">设定数据库密码</string> + <string name="receipts_groups_title_disable">为群组禁用回执吗?</string> + <string name="rcv_group_event_3_members_connected">%s、%s 和 %d 已连接</string> + <string name="fix_connection_not_supported_by_group_member">修复群组成员不支持的问题</string> + <string name="receipts_groups_override_enabled">已为 %d 组启用送达回执功能</string> + <string name="sync_connection_force_confirm">重新协商</string> + <string name="receipts_contacts_disable_keep_overrides">禁用(保留覆盖)</string> + <string name="set_database_passphrase">设置数据库密码</string> + <string name="receipts_contacts_override_disabled">已禁用 %d 联系人的送达回执功能</string> + <string name="receipts_groups_enable_keep_overrides">启用(保留组覆盖)</string> + <string name="system_restricted_background_warn"><![CDATA[要启用通知,请在应用设置中选择<b>应用电池使用情况</b> / <b>无限制</b>。]]></string> + <string name="send_receipts_disabled_alert_title">送达回执已禁用</string> + <string name="open_database_folder">打开数据库文件夹</string> + <string name="no_history">无历史记录</string> + <string name="fix_connection_confirm">修复</string> + <string name="fix_connection">修复连接</string> + <string name="rcv_group_event_2_members_connected">%s 和 %s 已连接</string> + <string name="send_receipts_disabled">关闭</string> + <string name="receipts_section_groups">小群组(最多 20 人)</string> + <string name="privacy_show_last_messages">显示最近的消息</string> + <string name="settings_section_title_delivery_receipts">将送达回执发送给</string> + <string name="error_enabling_delivery_receipts">启用已读回执时出错!</string> + <string name="passphrase_will_be_saved_in_settings">更改密码或重启应用后,密码将以明文形式保存在设置中。</string> + <string name="paste_the_link_you_received_to_connect_with_your_contact">粘贴您收到的链接以与您的联系人联系…</string> + <string name="send_receipts">送达回执</string> + <string name="no_selected_chat">没有选择聊天</string> + <string name="conn_event_ratchet_sync_ok">可以加密</string> + <string name="renegotiate_encryption">重新协商加密</string> + <string name="receipts_groups_disable_keep_overrides">禁用(保留组覆盖)</string> + <string name="receipts_groups_title_enable">为群组启用回执吗?</string> + <string name="fix_connection_not_supported_by_contact">修复联系人不支持的问题</string> + <string name="snd_conn_event_ratchet_sync_ok">对 %s 加密正常</string> + <string name="v5_2_fix_encryption_descr">修复还原备份后的加密问题。</string> + <string name="sync_connection_force_desc">加密正在运行,不需要新的加密协议。此操作可能会导致连接错误!</string> + <string name="disable_notifications_button">禁用通知</string> + <string name="in_reply_to">回复</string> + <string name="dont_enable_receipts">不启用</string> + <string name="connect_via_member_address_alert_desc">连接请求将发送给该组成员。</string> + <string name="settings_is_storing_in_clear_text">密码以明文形式存储在设置中。</string> + <string name="error_synchronizing_connection">同步连接时出错</string> + <string name="receipts_section_description">这些设置适用于您当前的配置文件</string> + <string name="snd_conn_event_ratchet_sync_allowed">允许为 %s 重新协商加密</string> + <string name="receipts_contacts_enable_for_all">为所有人启用</string> + <string name="conn_event_ratchet_sync_required">需要重新协商加密</string> + <string name="delivery_receipts_are_disabled">已关闭送达回执!</string> + <string name="socks_proxy_setting_limitations"><![CDATA[<b>请注意</b>:消息和文件中继通过 SOCKS 代理连接。呼叫和发送链接预览使用直接连接。]]></string> + <string name="encrypt_local_files">加密本地文件</string> + <string name="v5_3_encrypt_local_files">为存储的文件和媒体加密</string> + <string name="v5_3_new_desktop_app">全新桌面应用!</string> + <string name="v5_3_new_interface_languages">6种全新的界面语言</string> + <string name="v5_3_encrypt_local_files_descr">应用程序为新的本地文件(视频除外)加密。</string> + <string name="v5_3_discover_join_groups">发现和加入群组</string> + <string name="v5_3_simpler_incognito_mode">简化的隐身模式</string> + <string name="v5_3_new_interface_languages_descr">阿拉伯语、保加利亚语、芬兰语、希伯莱语、泰国语和乌克兰语——得益于用户和Weblate。</string> + <string name="v5_3_new_desktop_app_descr">在桌面应用里创建新的账号。💻</string> + <string name="v5_3_simpler_incognito_mode_descr">在连接时切换隐身模式。</string> + <string name="v5_3_discover_join_groups_descr">- 连接到目录服务(BETA)! +\n- 发送回执(至多20名成员)。 +\n- 更快,更稳定。</string> </resources> \ No newline at end of file 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 714e8fb8f5..24b02e64e3 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 @@ -29,7 +29,6 @@ <string name="ttl_sec">%d 秒</string> <string name="feature_cancelled_item">已取消 %s</string> <string name="v4_2_auto_accept_contact_requests">自動接受聯絡人請求</string> - <string name="group_unsupported_incognito_main_profile_sent">匿名聊天模式在這裡是不支援 - 你的個人檔案會顯示於群組內</string> <string name="chat_preferences_no">不</string> <string name="allow_to_send_disappearing">允許傳送自動銷毀的訊息。</string> <string name="database_initialization_error_title">無法初始化數據庫</string> @@ -185,7 +184,6 @@ <string name="star_on_github">於 Github 給個星星</string> <string name="incognito_info_protects">匿名聊天模式會保護你的真實個人檔案名稱和頭像 — 當有新聯絡人的時候會自動建立一個隨機性的個人檔案。</string> <string name="incognito_info_allows">這樣是允許每一個對話中擁有不同的顯示名稱,並且沒有任何的個人資料可用於分享或有機會外洩。</string> - <string name="incognito_info_find">若要查找用於匿名聊天模式連接的個人檔案,請點擊聯絡人或群組名稱。</string> <string name="allow_disappearing_messages_only_if">只有你的聯絡人允許的情況下,才允許自動銷毀訊息。</string> <string name="allow_your_contacts_to_send_disappearing_messages">允許你的聯絡人傳送自動銷毀的訊息。</string> <string name="allow_irreversible_message_deletion_only_if">只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。</string> @@ -476,9 +474,7 @@ <string name="if_you_cannot_meet_in_person_show_QR_in_video_call_or_via_another_channel"><![CDATA[如果你不能面對面接觸此聯絡人,<b>可於視訊通話中出示你的二維碼</b>,或者分享連結。]]></string> <string name="your_chat_profile_will_be_sent_to_your_contact">你的個人檔案會傳送給 \n你的聯絡人</string> - <string name="paste_connection_link_below_to_connect">將你接收到的連結貼上至下面的框內,以開啟你與你的聯絡人對話。</string> <string name="if_you_cannot_meet_in_person_scan_QR_in_video_call_or_ask_for_invitation_link"><![CDATA[如果你不能面對面接觸此聯絡人,你可以於 <b>視訊通話中掃描二維碼</b> ,或者你可以分享一個邀請連結給此聯絡人。]]></string> - <string name="your_profile_will_be_sent">你的個人檔案會傳送給你的聯絡人</string> <string name="paste_button">貼上</string> <string name="this_string_is_not_a_connection_link">這些字串不是連接連結!</string> <string name="you_can_also_connect_by_clicking_the_link"><![CDATA[你也可以點擊連結連接。如果在瀏覧器中開啟,點擊 <b>在電話應用程式內的開啟</b> 按鈕。]]></string> @@ -533,7 +529,7 @@ <string name="you_can_connect_to_simplex_chat_founder"><![CDATA[你可以透過 <font color="#0088ff">連接到 SimpleX Chat 開發人員提出任何問題並同意更新</font>。]]></string> <string name="to_start_a_new_chat_help_header">開啟新的對話</string> <string name="set_contact_name">設定聯絡人名稱</string> - <string name="you_invited_your_contact">你已邀請了你的聯絡人</string> + <string name="you_invited_a_contact">你已邀請了你的聯絡人</string> <string name="you_accepted_connection">你接受了連接</string> <string name="delete_pending_connection__question">刪除等待中的連接?</string> <string name="contact_you_shared_link_with_wont_be_able_to_connect">當聯絡人發現此連結後,嘗試點擊的聯絡人將無法連接!</string> @@ -756,7 +752,6 @@ <string name="users_delete_with_connections">檔案和伺服器連接</string> <string name="users_delete_data_only">只有本機檔案</string> <string name="incognito_random_profile">你的隨機個人檔案</string> - <string name="incognito_random_profile_description">隨機的個人檔案將傳送給你的聯絡人</string> <string name="theme_system">系統</string> <string name="theme_light">明亮</string> <string name="reset_color">重設顏色</string> @@ -827,7 +822,6 @@ <string name="conn_stats_section_title_servers">伺服器</string> <string name="receiving_via">接收訊息透過</string> <string name="sending_via">傳送訊息透過</string> - <string name="incognito_random_profile_from_contact_description">隨機的個人檔案將傳送給收到此連結的聯絡人</string> <string name="prohibit_sending_disappearing_messages">禁止傳送自動銷毀的訊息。</string> <string name="prohibit_message_deletion">禁止不可逆的訊息刪除。</string> <string name="prohibit_sending_voice">禁止傳送語音訊息。</string> diff --git a/apps/multiplatform/common/src/commonMain/resources/distribute/simplex.png b/apps/multiplatform/common/src/commonMain/resources/distribute/simplex.png deleted file mode 100644 index 6ab60a3293..0000000000 Binary files a/apps/multiplatform/common/src/commonMain/resources/distribute/simplex.png and /dev/null differ diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt new file mode 100644 index 0000000000..62a4d9e214 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -0,0 +1,162 @@ +package chat.simplex.common + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.* +import chat.simplex.common.model.ChatController +import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.defaultLocale +import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.views.helpers.FileDialogChooser +import kotlinx.coroutines.* +import java.awt.event.WindowEvent +import java.awt.event.WindowFocusListener +import java.io.File + +val simplexWindowState = SimplexWindowState() + +fun showApp() = application { + // TODO: remove after update to compose 1.5.0+ + // See: https://github.com/JetBrains/compose-multiplatform/issues/3366#issuecomment-1643799976 + System.setProperty("compose.scrolling.smooth.enabled", "false") + + // For some reason on Linux actual width will be 10.dp less after specifying it here. If we specify 1366, + // it will show 1356. But after that we can still update it to 1366 by changing window state. Just making it +10 now here + val width = if (desktopPlatform.isLinux()) 1376.dp else 1366.dp + val windowState = rememberWindowState(placement = WindowPlacement.Floating, width = width, height = 768.dp) + simplexWindowState.windowState = windowState + // Reload all strings in all @Composable's after language change at runtime + if (remember { ChatController.appPrefs.appLanguage.state }.value != "") { + Window(state = windowState, onCloseRequest = ::exitApplication, onKeyEvent = { + if (it.key == Key.Escape && it.type == KeyEventType.KeyUp) { + simplexWindowState.backstack.lastOrNull()?.invoke() != null + } else { + false + } + }, title = "SimpleX") { + SimpleXTheme { + AppScreen() + if (simplexWindowState.openDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = true, + params = simplexWindowState.openDialog.params, + onResult = { + simplexWindowState.openDialog.onResult(it.firstOrNull()) + } + ) + } + + if (simplexWindowState.openMultipleDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = true, + params = simplexWindowState.openMultipleDialog.params, + onResult = { + simplexWindowState.openMultipleDialog.onResult(it) + } + ) + } + + if (simplexWindowState.saveDialog.isAwaiting) { + FileDialogChooser( + title = "SimpleX", + isLoad = false, + params = simplexWindowState.saveDialog.params, + onResult = { simplexWindowState.saveDialog.onResult(it.firstOrNull()) } + ) + } + val toasts = remember { simplexWindowState.toasts } + val toast = toasts.firstOrNull() + if (toast != null) { + Box(Modifier.fillMaxSize().padding(bottom = 20.dp), contentAlignment = Alignment.BottomCenter) { + Text( + toast.first, + Modifier.background(MaterialTheme.colors.primary, RoundedCornerShape(100)).padding(vertical = 5.dp, horizontal = 10.dp), + color = MaterialTheme.colors.onPrimary, + style = MaterialTheme.typography.body1 + ) + } + // Shows toast in insertion order with preferred delay per toast. New one will be shown once previous one expires + LaunchedEffect(toast, toasts.size) { + delay(toast.second) + simplexWindowState.toasts.removeFirst() + } + } + } + var windowFocused by remember { simplexWindowState.windowFocused } + LaunchedEffect(windowFocused) { + val delay = ChatController.appPrefs.laLockDelay.get() + if (!windowFocused && ChatModel.performLA.value && delay > 0) { + delay(delay * 1000L) + // Trigger auth state check when delay ends (and if it ends) + AppLock.recheckAuthState() + } + } + LaunchedEffect(Unit) { + window.addWindowFocusListener(object: WindowFocusListener { + override fun windowGainedFocus(p0: WindowEvent?) { + windowFocused = true + AppLock.recheckAuthState() + } + + override fun windowLostFocus(p0: WindowEvent?) { + windowFocused = false + AppLock.appWasHidden() + } + }) + } + } + } +} + +class SimplexWindowState { + lateinit var windowState: WindowState + val backstack = mutableStateListOf<() -> Unit>() + val openDialog = DialogState<File?>() + val openMultipleDialog = DialogState<List<File>>() + val saveDialog = DialogState<File?>() + val toasts = mutableStateListOf<Pair<String, Long>>() + var windowFocused = mutableStateOf(true) +} + +data class DialogParams( + val filename: String? = null, + val allowMultiple: Boolean = false, + val fileFilter: ((File?) -> Boolean)? = null, + val fileFilterDescription: String = "", +) + +class DialogState<T> { + private var onResult: CompletableDeferred<T>? by mutableStateOf(null) + var params = DialogParams() + val isAwaiting get() = onResult != null + + suspend fun awaitResult(params: DialogParams = DialogParams()): T { + onResult = CompletableDeferred() + this.params = params + val result = onResult!!.await() + onResult = null + return result + } + + fun onResult(result: T) = onResult!!.complete(result) +} + +@Preview +@Composable +fun AppPreview() { + SimpleXTheme { + AppScreen() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt new file mode 100644 index 0000000000..94e985328e --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -0,0 +1,157 @@ +package chat.simplex.common.model + +import androidx.compose.ui.graphics.* +import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import com.sshtools.twoslices.* +import java.awt.* +import java.awt.TrayIcon.MessageType +import java.io.File +import javax.imageio.ImageIO + +object NtfManager { + private val prevNtfs = arrayListOf<Pair<ChatId, Slice>>() + + fun notifyCallInvitation(invitation: RcvCallInvitation) { + if (simplexWindowState.windowFocused.value) return + val contactId = invitation.contact.id + Log.d(TAG, "notifyCallInvitation $contactId") + val image = invitation.contact.image + val text = generalGetString( + if (invitation.callType.media == CallMediaType.Video) { + if (invitation.sharedKey == null) MR.strings.video_call_no_encryption else MR.strings.encrypted_video_call + } else { + if (invitation.sharedKey == null) MR.strings.audio_call_no_encryption else MR.strings.encrypted_audio_call + } + ) + val previewMode = appPreferences.notificationPreviewMode.get() + val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) + generalGetString(MR.strings.notification_preview_somebody) + else + invitation.contact.displayName + val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name) + MR.images.icon_foreground_common.image.toComposeImageBitmap() + else + base64ToBitmap(image) + + val actions = listOf( + generalGetString(MR.strings.accept) to { ntfManager.acceptCallAction(invitation.contact.id) }, + generalGetString(MR.strings.reject) to { ChatModel.callManager.endCall(invitation = invitation) } + ) + displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) { + ntfManager.openChatAction(invitation.user.userId, contactId) + } + } + + fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } + + fun cancelNotificationsForChat(chatId: ChatId) { + val ntf = prevNtfs.firstOrNull { it.first == chatId } + if (ntf != null) { + prevNtfs.remove(ntf) + /*try { + ntf.second.close() + } catch (e: Exception) { + // Can be java.lang.UnsupportedOperationException, for example. May do nothing + println("Failed to close notification: ${e.stackTraceToString()}") + }*/ + } + } + + fun cancelAllNotifications() { +// prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } } + prevNtfs.clear() + } + + fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) { + if (!user.showNotifications) return + Log.d(TAG, "notifyMessageReceived $chatId") + val previewMode = appPreferences.notificationPreviewMode.get() + val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName + val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText + val largeIcon = when { + actions.isEmpty() -> null + image == null || previewMode == NotificationPreviewMode.HIDDEN.name -> MR.images.icon_foreground_common.image.toComposeImageBitmap() + else -> base64ToBitmap(image) + } + + displayNotificationViaLib(chatId, title, content, prepareIconPath(largeIcon), actions.map { it.first.name to it.second }) { + ntfManager.openChatAction(user.userId, chatId) + } + } + + private fun displayNotificationViaLib( + chatId: String, + title: String, + text: String, + iconPath: String?, + actions: List<Pair<String, () -> Unit>>, + defaultAction: (() -> Unit)? + ) { + val builder = Toast.builder() + .title(title) + .content(text) + if (iconPath != null) { + builder.icon(iconPath) + } + if (defaultAction != null) { + builder.defaultAction(defaultAction) + } + actions.forEach { + builder.action(it.first, it.second) + } + try { + prevNtfs.add(chatId to builder.toast()) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } + + private fun prepareIconPath(icon: ImageBitmap?): String? = if (icon != null) { + tmpDir.mkdir() + val newFile = File(tmpDir.absolutePath + File.separator + generateNewFileName("IMG", "png")) + try { + ImageIO.write(icon.toAwtImage(), "PNG", newFile.outputStream()) + newFile.absolutePath + } catch (e: Exception) { + println("Failed to write an icon to tmpDir: ${e.stackTraceToString()}") + null + } + } else null + + private fun displayNotification(title: String, text: String, icon: ImageBitmap?) = when (desktopPlatform) { + DesktopPlatform.LINUX_X86_64, DesktopPlatform.LINUX_AARCH64 -> linuxDisplayNotification(title, text, prepareIconPath(icon)) + DesktopPlatform.WINDOWS_X86_64 -> windowsDisplayNotification(title, text, icon) + DesktopPlatform.MAC_X86_64, DesktopPlatform.MAC_AARCH64 -> macDisplayNotification(title, text, prepareIconPath(icon)) + } + + private fun linuxDisplayNotification(title: String, text: String, iconPath: String?) { + if (iconPath != null) { + Runtime.getRuntime().exec(arrayOf("notify-send", "-i", iconPath, title, text)) + } else { + Toast.toast(ToastType.INFO, title, text) + Runtime.getRuntime().exec(arrayOf("notify-send", title, text)) + } + } + + private fun windowsDisplayNotification(title: String, text: String, icon: ImageBitmap?) { + if (SystemTray.isSupported()) { + val tray = SystemTray.getSystemTray() + tray.remove(tray.trayIcons.firstOrNull { it.toolTip == "SimpleX" }) + val trayIcon = TrayIcon(icon?.toAwtImage(), "SimpleX") + trayIcon.isImageAutoSize = true + tray.add(trayIcon) + trayIcon.displayMessage(title, text, MessageType.INFO) + } else { + Log.e(TAG, "System tray not supported!") + } + } + + private fun macDisplayNotification(title: String, text: String, iconPath: String?) { + Runtime.getRuntime().exec(arrayOf("osascript", "-e", """display notification "${text.replace("\"", "\\\"")}" with title "${title.replace("\"", "\\\"")}"""")) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt new file mode 100644 index 0000000000..7193fbe2be --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -0,0 +1,40 @@ +package chat.simplex.common.platform + +import chat.simplex.common.model.* +import chat.simplex.common.views.call.RcvCallInvitation +import chat.simplex.common.views.helpers.withBGApi +import java.util.* + +actual val appPlatform = AppPlatform.DESKTOP + +@Suppress("ConstantLocale") +val defaultLocale: Locale = Locale.getDefault() + +fun initApp() { + ntfManager = object : NtfManager() { + override fun notifyCallInvitation(invitation: RcvCallInvitation) = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation) + override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId) + override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId) + override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List<Pair<NotificationAction, () -> Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions) + override fun androidCreateNtfChannelsMaybeShowAlert() {} + override fun cancelCallNotification() {} + override fun cancelAllNotifications() = chat.simplex.common.model.NtfManager.cancelAllNotifications() + } + applyAppLocale() + withBGApi { + initChatController() + runMigrations() + } + // LALAL + //testCrypto() +} + +fun discoverVlcLibs(path: String) { + uk.co.caprica.vlcj.binding.LibC.INSTANCE.setenv("VLC_PLUGIN_PATH", path, 1) +} + +private fun applyAppLocale() { + val lang = ChatController.appPrefs.appLanguage.get() + if (lang == null || lang == Locale.getDefault().language) return + Locale.setDefault(Locale.forLanguageTag(lang)) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Back.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Back.desktop.kt new file mode 100644 index 0000000000..7c64cd469a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Back.desktop.kt @@ -0,0 +1,14 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* +import chat.simplex.common.simplexWindowState + +@Composable +actual fun BackHandler(enabled: Boolean, onBack: () -> Unit) { + DisposableEffect(enabled) { + if (enabled) simplexWindowState.backstack.add(onBack) + onDispose { + simplexWindowState.backstack.remove(onBack) + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt new file mode 100644 index 0000000000..870fde945b --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt @@ -0,0 +1,15 @@ +package chat.simplex.common.platform + +actual val cryptor: CryptorInterface = object : CryptorInterface { + override fun decryptData(data: ByteArray, iv: ByteArray, alias: String): String? { + return String(data) // LALAL + } + + override fun encryptText(text: String, alias: String): Pair<ByteArray, ByteArray> { + return text.toByteArray() to text.toByteArray() // LALAL + } + + override fun deleteKey(alias: String) { + // LALAL + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt new file mode 100644 index 0000000000..46124a44fa --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -0,0 +1,108 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* +import chat.simplex.common.* +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import java.awt.Desktop +import java.io.* +import java.net.URI + +actual val dataDir: File = File(desktopPlatform.dataPath) +actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } +actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files") +actual val appFilesDir: File = filesDir +actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") +actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" + +actual val chatDatabaseFileName: String = "simplex_v1_chat.db" +actual val agentDatabaseFileName: String = "simplex_v1_agent.db" + +actual val databaseExportDir: File = tmpDir + +val vlcDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex-vlc").also { it.deleteOnExit() } + +actual fun desktopOpenDatabaseDir() { + if (Desktop.isDesktopSupported()) { + try { + Desktop.getDesktop().open(dataDir); + } catch (e: IOException) { + Log.e(TAG, e.stackTraceToString()) + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.unknown_error), + text = e.stackTraceToString() + ) + } + } +} + +@Composable +actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher = + remember(rememberedValue) { FileChooserLauncher(getContent, onResult) } + +@Composable +actual fun rememberFileChooserMultipleLauncher(onResult: (List<URI>) -> Unit): FileChooserMultipleLauncher = + remember { FileChooserMultipleLauncher(onResult) } + +actual class FileChooserLauncher actual constructor() { + var getContent: Boolean = false + lateinit var onResult: (URI?) -> Unit + + constructor(getContent: Boolean, onResult: (URI?) -> Unit): this() { + this.getContent = getContent + this.onResult = onResult + } + + actual suspend fun launch(input: String) { + var res: File? + if (getContent) { + val params = DialogParams( + allowMultiple = false, + fileFilter = fileFilter(input), + fileFilterDescription = fileFilterDescription(input), + ) + res = simplexWindowState.openDialog.awaitResult(params) + } else { + res = simplexWindowState.saveDialog.awaitResult(DialogParams(filename = input)) + if (res != null && res.isDirectory) { + res = File(res, input) + } + } + onResult(res?.toURI()) + } +} + +actual class FileChooserMultipleLauncher actual constructor() { + lateinit var onResult: (List<URI>) -> Unit + + constructor(onResult: (List<URI>) -> Unit): this() { + this.onResult = onResult + } + + actual suspend fun launch(input: String) { + val params = DialogParams( + allowMultiple = true, + fileFilter = fileFilter(input), + fileFilterDescription = fileFilterDescription(input), + ) + onResult(simplexWindowState.openMultipleDialog.awaitResult(params).map { it.toURI() }) + } +} + +private fun fileFilter(input: String): (File?) -> Boolean = when(input) { + "image/*" -> { file -> if (file?.isDirectory == true) true else if (file != null) isImage(file.toURI()) else false } + "video/*" -> { file -> if (file?.isDirectory == true) true else if (file != null) isVideo(file.toURI()) else false } + "*/*" -> { _ -> true } + else -> { _ -> true } +} + +private fun fileFilterDescription(input: String): String = when(input) { + "image/*" -> generalGetString(MR.strings.gallery_image_button) + "video/*" -> generalGetString(MR.strings.gallery_video_button) + "*/*" -> generalGetString(MR.strings.choose_file) + else -> "" +} + +actual fun URI.inputStream(): InputStream? = File(URI("file:" + toString().removePrefix("file:"))).inputStream() +actual fun URI.outputStream(): OutputStream = File(URI("file:" + toString().removePrefix("file:"))).outputStream() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt new file mode 100644 index 0000000000..b7341cb4a8 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Images.desktop.kt @@ -0,0 +1,173 @@ +package chat.simplex.common.platform + +import androidx.compose.ui.graphics.* +import boofcv.io.image.ConvertBufferedImage +import boofcv.struct.image.GrayU8 +import chat.simplex.res.MR +import org.jetbrains.skia.Image +import java.awt.RenderingHints +import java.awt.image.BufferedImage +import java.io.* +import java.net.URI +import java.util.* +import javax.imageio.* +import javax.imageio.stream.MemoryCacheImageOutputStream +import kotlin.math.sqrt + +private fun errorBitmap(): ImageBitmap = + ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg=="))).toComposeImageBitmap() + +actual fun base64ToBitmap(base64ImageString: String): ImageBitmap { + val imageString = base64ImageString + .removePrefix("data:image/png;base64,") + .removePrefix("data:image/jpg;base64,") + return try { + ImageIO.read(ByteArrayInputStream(Base64.getDecoder().decode(imageString))).toComposeImageBitmap() + } catch (e: IOException) { + Log.e(TAG, "base64ToBitmap error: $e") + errorBitmap() + } +} + +actual fun resizeImageToStrSize(image: ImageBitmap, maxDataSize: Long): String { + var img = image + var str = compressImageStr(img) + while (str.length > maxDataSize) { + val ratio = sqrt(str.length.toDouble() / maxDataSize.toDouble()) + val clippedRatio = kotlin.math.min(ratio, 2.0) + val width = (img.width.toDouble() / clippedRatio).toInt() + val height = img.height * width / img.width + img = img.scale(width, height) + str = compressImageStr(img) + } + return str +} +actual fun resizeImageToDataSize(image: ImageBitmap, usePng: Boolean, maxDataSize: Long): ByteArrayOutputStream { + var img = image + var stream = compressImageData(img, usePng) + while (stream.size() > maxDataSize) { + val ratio = sqrt(stream.size().toDouble() / maxDataSize.toDouble()) + val clippedRatio = kotlin.math.min(ratio, 2.0) + val width = (img.width.toDouble() / clippedRatio).toInt() + val height = img.height * width / img.width + img = img.scale(width, height) + stream = compressImageData(img, usePng) + } + return stream +} + +actual fun cropToSquare(image: ImageBitmap): ImageBitmap { + var xOffset = 0 + var yOffset = 0 + val side = kotlin.math.min(image.height, image.width) + if (image.height < image.width) { + xOffset = (image.width - side) / 2 + } else { + yOffset = (image.height - side) / 2 + } + // LALAL MAKE REAL CROP + return image +} + +actual fun compressImageStr(bitmap: ImageBitmap): String { + val usePng = bitmap.hasAlpha() + val ext = if (usePng) "png" else "jpg" + return try { + val encoded = Base64.getEncoder().encodeToString(compressImageData(bitmap, usePng).toByteArray()) + "data:image/$ext;base64,$encoded" + } catch (e: IOException) { + Log.e(TAG, "resizeImageToStrSize error: $e") + throw e + } +} + +actual fun compressImageData(bitmap: ImageBitmap, usePng: Boolean): ByteArrayOutputStream { + val writer = ImageIO.getImageWritersByFormatName(if (usePng) "png" else "jpg").next() + val writeParam = writer.defaultWriteParam + writeParam.compressionMode = ImageWriteParam.MODE_EXPLICIT + writeParam.compressionQuality = 0.85f + val stream = ByteArrayOutputStream() + writer.output = MemoryCacheImageOutputStream(stream) + val outputImage = IIOImage(if (usePng) bitmap.toAwtImage() else removeAlphaChannel(bitmap.toAwtImage()), null, null) + writer.write(null, outputImage, writeParam) + writer.dispose() + stream.flush() + return stream +} + +private fun removeAlphaChannel(img: BufferedImage): BufferedImage { + if (!img.colorModel.hasAlpha()) return img + val target = BufferedImage(img.width, img.height, BufferedImage.TYPE_INT_RGB) + val g = target.createGraphics() + g.fillRect(0, 0, img.width, img.height) + g.drawImage(img, 0, 0, null) + g.dispose() + return target +} + +actual fun GrayU8.toImageBitmap(): ImageBitmap = ConvertBufferedImage.extractBuffered(this).toComposeImageBitmap() + +actual fun ImageBitmap.hasAlpha(): Boolean { + val map = toPixelMap() + var y = 0 + while (y < height) { + var x = 0 + while (x < width) { + if (map[x, y].alpha < 1f) return true + x++ + } + y++ + } + return false +} + +actual fun ImageBitmap.addLogo(): ImageBitmap { + val radius = (width * 0.16f).toInt() + val logoSize = (width * 0.24).toInt() + val logo: BufferedImage = MR.images.icon_foreground_common.image + val original = toAwtImage() + val withLogo = BufferedImage(width, height, original.type) + val g = withLogo.createGraphics() + g.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR + ) + g.drawImage(original, 0, 0, width, height, 0, 0, original.width, original.height, null) + g.fillRoundRect(width / 2 - radius / 2, height / 2 - radius / 2, radius, radius, radius, radius) + g.drawImage(logo, (width - logoSize) / 2, (height - logoSize) / 2, logoSize, logoSize, null) + g.dispose() + + return withLogo.toComposeImageBitmap() +} + +actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap { + val original = toAwtImage() + val resized = BufferedImage(width, height, original.type) + val g = resized.createGraphics() + g.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR + ) + g.drawImage(original, 0, 0, width, height, 0, 0, original.width, original.height, null) + g.dispose() + return resized.toComposeImageBitmap() +} + +// LALAL +actual fun isImage(uri: URI): Boolean { + val path = uri.path.lowercase() + return path.endsWith(".gif") || + path.endsWith(".webp") || + path.endsWith(".png") || + path.endsWith(".jpg") || + path.endsWith(".jpeg") +} + +actual fun isAnimImage(uri: URI, drawable: Any?): Boolean { + val path = uri.path.lowercase() + return path.endsWith(".gif") || path.endsWith(".webp") +} + +@Suppress("NewApi") +actual fun loadImageBitmap(inputStream: InputStream): ImageBitmap = + Image.makeFromEncoded(inputStream.readAllBytes()).toComposeImageBitmap() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt new file mode 100644 index 0000000000..395754c51b --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Log.desktop.kt @@ -0,0 +1,8 @@ +package chat.simplex.common.platform + +actual object Log { + actual fun d(tag: String, text: String) = println("D: $text") + actual fun e(tag: String, text: String) = println("E: $text") + actual fun i(tag: String, text: String) = println("I: $text") + actual fun w(tag: String, text: String) = println("W: $text") +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt new file mode 100644 index 0000000000..0185a50fcf --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -0,0 +1,31 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.* +import androidx.compose.ui.graphics.painter.Painter + +actual fun Modifier.navigationBarsWithImePadding(): Modifier = this + +@Composable +actual fun ProvideWindowInsets( + consumeWindowInsets: Boolean, + windowInsetsAnimationsEnabled: Boolean, + content: @Composable () -> Unit +) { + content() +} + +@Composable +actual fun Modifier.desktopOnExternalDrag( + enabled: Boolean, + onFiles: (List<String>) -> Unit, + onImage: (Painter) -> Unit, + onText: (String) -> Unit +): Modifier = +onExternalDrag(enabled) { + when(val data = it.dragData) { + is DragData.FilesList -> onFiles(data.readFiles()) + is DragData.Image -> onImage(data.readImage()) + is DragData.Text -> onText(data.readText()) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt new file mode 100644 index 0000000000..397f8a6966 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Notifications.desktop.kt @@ -0,0 +1,5 @@ +package chat.simplex.common.platform + +import chat.simplex.common.simplexWindowState + +actual fun allowedToShowNotification(): Boolean = !simplexWindowState.windowFocused.value diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt new file mode 100644 index 0000000000..2121b9cfdf --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt @@ -0,0 +1,33 @@ +package chat.simplex.common.platform + +import java.io.File +import java.util.* + +private val home = System.getProperty("user.home") +private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config") + "/simplex" +private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex" +val desktopPlatform = detectDesktopPlatform() + +enum class DesktopPlatform(val libPath: String, val libExtension: String, val configPath: String, val dataPath: String) { + LINUX_X86_64("/libs/linux-x86_64", "so", unixConfigPath, unixDataPath), + LINUX_AARCH64("/libs/aarch64", "so", unixConfigPath, unixDataPath), + WINDOWS_X86_64("/libs/windows-x86_64", "dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), + MAC_X86_64("/libs/mac-x86_64", "dylib", unixConfigPath, unixDataPath), + MAC_AARCH64("/libs/mac-aarch64", "dylib", unixConfigPath, unixDataPath); + + fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 + fun isMac() = this == MAC_X86_64 || this == MAC_AARCH64 +} + +private fun detectDesktopPlatform(): DesktopPlatform { + val os = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH) + val arch = System.getProperty("os.arch") + return when { + os == "linux" && (arch.contains("x86") || arch == "amd64") -> DesktopPlatform.LINUX_X86_64 + os == "linux" && arch == "aarch64" -> DesktopPlatform.LINUX_AARCH64 + os.contains("windows") && (arch.contains("x86") || arch == "amd64") -> DesktopPlatform.WINDOWS_X86_64 + os.contains("mac") && arch.contains("x86") -> DesktopPlatform.MAC_X86_64 + os.contains("mac") && arch.contains("aarch64") -> DesktopPlatform.MAC_AARCH64 + else -> TODO("Currently, your processor's architecture ($arch) or os ($os) are unsupported. Please, contact us") + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt new file mode 100644 index 0000000000..3b7ba84863 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -0,0 +1,144 @@ +package chat.simplex.common.platform + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.input.key.* +import androidx.compose.ui.platform.* +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import dev.icerock.moko.resources.StringResource +import kotlinx.coroutines.delay +import kotlin.math.min +import kotlin.text.substring + +@Composable +actual fun PlatformTextField( + composeState: MutableState<ComposeState>, + sendMsgEnabled: Boolean, + textStyle: MutableState<TextStyle>, + showDeleteTextButton: MutableState<Boolean>, + userIsObserver: Boolean, + onMessageChange: (String) -> Unit, + onUpArrow: () -> Unit, + onDone: () -> Unit, +) { + val cs = composeState.value + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + val keyboard = LocalSoftwareKeyboardController.current + val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp) + LaunchedEffect(cs.contextItem) { + if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect + // In replying state + focusRequester.requestFocus() + delay(50) + keyboard?.show() + } + LaunchedEffect(sendMsgEnabled) { + if (!sendMsgEnabled) { + focusManager.clearFocus() + delay(50) + keyboard?.hide() + } + } + val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) } + var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } + val textFieldValue = textFieldValueState.copy(text = cs.message) + BasicTextField( + value = textFieldValue, + onValueChange = { + if (!composeState.value.inProgress && !(composeState.value.preview is ComposePreview.VoicePreview && it.text != "")) { + textFieldValueState = it + onMessageChange(it.text) + } + }, + textStyle = textStyle.value, + maxLines = 16, + keyboardOptions = KeyboardOptions.Default.copy( + capitalization = KeyboardCapitalization.Sentences, + autoCorrect = true + ), + modifier = Modifier + .padding(vertical = 4.dp) + .focusRequester(focusRequester) + .onPreviewKeyEvent { + if (it.key == Key.Enter && it.type == KeyEventType.KeyDown) { + if (it.isShiftPressed) { + val start = if (minOf(textFieldValue.selection.min) == 0) "" else textFieldValue.text.substring(0 until textFieldValue.selection.min) + val newText = start + "\n" + + textFieldValue.text.substring(textFieldValue.selection.max, textFieldValue.text.length) + textFieldValueState = textFieldValue.copy( + text = newText, + selection = TextRange(textFieldValue.selection.min + 1) + ) + onMessageChange(newText) + } else if (cs.message.isNotEmpty()) { + onDone() + } + true + } else if (it.key == Key.DirectionUp && it.type == KeyEventType.KeyDown && cs.message.isEmpty()) { + onUpArrow() + true + } + else false + }, + cursorBrush = SolidColor(MaterialTheme.colors.secondary), + decorationBox = { innerTextField -> + Surface( + shape = RoundedCornerShape(18.dp), + border = BorderStroke(1.dp, MaterialTheme.colors.secondary) + ) { + Row( + Modifier.background(MaterialTheme.colors.background), + verticalAlignment = Alignment.Bottom + ) { + CompositionLocalProvider( + LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current + ) { + Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) { + Spacer(Modifier.height(8.dp)) + innerTextField() + Spacer(Modifier.height(10.dp)) + } + } + } + } + }, + + ) + showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress + if (composeState.value.preview is ComposePreview.VoicePreview) { + ComposeOverlay(MR.strings.voice_message_send_text, textStyle, padding) + } else if (userIsObserver) { + ComposeOverlay(MR.strings.you_are_observer, textStyle, padding) + } +} + +@Composable +private fun ComposeOverlay(textId: StringResource, textStyle: MutableState<TextStyle>, padding: PaddingValues) { + Text( + generalGetString(textId), + Modifier.padding(padding), + color = MaterialTheme.colors.secondary, + style = textStyle.value.copy(fontStyle = FontStyle.Italic) + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt new file mode 100644 index 0000000000..ed8efcd57f --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -0,0 +1,213 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.AlertManager +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR +import kotlinx.coroutines.* +import uk.co.caprica.vlcj.player.base.MediaPlayer +import uk.co.caprica.vlcj.player.base.State +import uk.co.caprica.vlcj.player.component.AudioPlayerComponent +import java.io.File +import kotlin.math.max + +actual class RecorderNative: RecorderInterface { + override fun start(onProgressUpdate: (position: Int?, finished: Boolean) -> Unit): String { + /*LALAL*/ + return "" + } + + override fun stop(): Int { + /*LALAL*/ + return 0 + } +} + +actual object AudioPlayer: AudioPlayerInterface { + val player by lazy { AudioPlayerComponent().mediaPlayer() } + + // Filepath: String, onProgressUpdate + private val currentlyPlaying: MutableState<Pair<CryptoFile, (position: Int?, state: TrackState) -> Unit>?> = mutableStateOf(null) + private var progressJob: Job? = null + + enum class TrackState { + PLAYING, PAUSED, REPLACED + } + + // Returns real duration of the track + private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + val absoluteFilePath = getAppFilePath(fileSource.filePath) + if (!File(absoluteFilePath).exists()) { + Log.e(TAG, "No such file: ${fileSource.filePath}") + return null + } + + VideoPlayerHolder.stopAll() + RecorderInterface.stopRecording?.invoke() + val current = currentlyPlaying.value + if (current == null || current.first != fileSource) { + stopListener() + player.stop() + runCatching { + if (fileSource.cryptoArgs != null) { + val tmpFile = fileSource.createTmpFileIfNeeded() + decryptCryptoFile(absoluteFilePath, fileSource.cryptoArgs, tmpFile.absolutePath) + player.media().prepare("file://${tmpFile.absolutePath}") + } else { + player.media().prepare("file://$absoluteFilePath") + } + }.onFailure { + Log.e(TAG, it.stackTraceToString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + return null + } + } + if (seek != null) player.seekTo(seek) + player.start() + currentlyPlaying.value = fileSource to onProgressUpdate + progressJob = CoroutineScope(Dispatchers.Default).launch { + onProgressUpdate(player.currentPosition, TrackState.PLAYING) + while(isActive && (player.isPlaying || player.status().state() == State.OPENING)) { + // Even when current position is equal to duration, the player has isPlaying == true for some time, + // so help to make the playback stopped in UI immediately + if (player.currentPosition == player.duration) { + onProgressUpdate(player.currentPosition, TrackState.PLAYING) + break + } + delay(50) + onProgressUpdate(player.currentPosition, TrackState.PLAYING) + } + onProgressUpdate(null, TrackState.PAUSED) + currentlyPlaying.value?.first?.deleteTmpFile() + } + return player.duration + } + + private fun pause(): Int { + progressJob?.cancel() + progressJob = null + val position = player.currentPosition + player.pause() + return position + } + + override fun stop() { + if (currentlyPlaying.value == null) return + player.stop() + stopListener() + } + + override fun stop(item: ChatItem) = stop(item.file?.fileName) + + // FileName or filePath are ok + override fun stop(fileName: String?) { + if (fileName != null && currentlyPlaying.value?.first?.filePath?.endsWith(fileName) == true) { + stop() + } + } + + private fun stopListener() { + val afterCoroutineCancel: CompletionHandler = { + // Notify prev audio listener about stop + currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value = null + } + /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: + * [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.REPLACED] (in this order) + * */ + if (progressJob != null) { + progressJob?.invokeOnCompletion(afterCoroutineCancel) + } else { + afterCoroutineCancel(null) + } + progressJob?.cancel() + progressJob = null + } + + override fun play( + fileSource: CryptoFile, + audioPlaying: MutableState<Boolean>, + progress: MutableState<Int>, + duration: MutableState<Int>, + resetOnEnd: Boolean, + ) { + if (progress.value == duration.value) { + progress.value = 0 + } + val realDuration = start(fileSource, progress.value) { pro, state -> + if (pro != null) { + progress.value = pro + } + if (pro == null || pro == duration.value) { + audioPlaying.value = false + if (pro == duration.value) { + progress.value = if (resetOnEnd) 0 else duration.value + } else if (state == TrackState.REPLACED) { + progress.value = 0 + } + } + } + audioPlaying.value = realDuration != null + // Update to real duration instead of what was received in ChatInfo + realDuration?.let { duration.value = it } + } + + override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) { + pro.value = pause() + audioPlaying.value = false + } + + override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { + pro.value = ms + if (currentlyPlaying.value?.first?.filePath == filePath) { + player.seekTo(ms) + } + } + + override fun duration(unencryptedFilePath: String): Int? { + var res: Int? = null + try { + val helperPlayer = AudioPlayerComponent().mediaPlayer() + helperPlayer.media().startPaused("file://$unencryptedFilePath") + res = helperPlayer.duration + helperPlayer.stop() + helperPlayer.release() + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + return res + } +} + +val MediaPlayer.isPlaying: Boolean + get() = status().isPlaying + +fun MediaPlayer.seekTo(time: Int) { + controls().setTime(time.toLong()) +} + +fun MediaPlayer.start() { + controls().start() +} + +fun MediaPlayer.pause() { + controls().pause() +} + +fun MediaPlayer.stop() { + controls().stop() +} + +private val MediaPlayer.currentPosition: Int + get() = max(0, status().time().toInt()) + +val MediaPlayer.duration: Int + get() = media().info().duration().toInt() + +actual object SoundPlayer: SoundPlayerInterface { + override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ } + override fun stop() { /*LALAL*/ } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt new file mode 100644 index 0000000000..b758988227 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -0,0 +1,60 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.* +import chat.simplex.common.simplexWindowState +import com.russhwolf.settings.* +import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.desc.desc +import java.io.File +import java.util.* + +@Composable +actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = + androidx.compose.ui.text.platform.Font("MR/fonts/$res.ttf", weight, style) + +actual fun StringResource.localized(): String = desc().toString() + +actual fun isInNightMode() = false + +private val settingsFile = + File(desktopPlatform.configPath + File.separator + "settings.properties") + .also { it.parentFile.mkdirs() } +private val settingsThemesFile = + File(desktopPlatform.configPath + File.separator + "themes.properties") + .also { it.parentFile.mkdirs() } +private val settingsProps = + Properties() + .also { try { it.load(settingsFile.reader()) } catch (e: Exception) { Properties() } } +private val settingsThemesProps = + Properties() + .also { try { it.load(settingsThemesFile.reader()) } catch (e: Exception) { Properties() } } + +actual val settings: Settings = PropertiesSettings(settingsProps) { settingsProps.store(settingsFile.writer(), "") } +actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { settingsThemesProps.store(settingsThemesFile.writer(), "") } + +actual fun windowOrientation(): WindowOrientation = + if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) { + WindowOrientation.LANDSCAPE + } else { + WindowOrientation.PORTRAIT + } + +@Composable +actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width + +actual fun desktopExpandWindowToWidth(width: Dp) { + if (simplexWindowState.windowState.size.width >= width) return + simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width) +} + +actual fun isRtl(text: CharSequence): Boolean { + if (text.isEmpty()) return false + return text.any { char -> + val dir = Character.getDirectionality(char) + dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt new file mode 100644 index 0000000000..1d5ab45bbb --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Share.desktop.kt @@ -0,0 +1,37 @@ +package chat.simplex.common.platform + +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.UriHandler +import androidx.compose.ui.text.AnnotatedString +import chat.simplex.common.model.* +import chat.simplex.common.views.helpers.getAppFileUri +import chat.simplex.common.views.helpers.withApi +import java.io.File +import java.net.URI +import java.net.URLEncoder +import chat.simplex.res.MR + +actual fun UriHandler.sendEmail(subject: String, body: CharSequence) { + val subjectEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20") + val bodyEncoded = URLEncoder.encode(subject, "UTF-8").replace("+", "%20") + openUri("mailto:?subject=$subjectEncoded&body=$bodyEncoded") +} + +actual fun ClipboardManager.shareText(text: String) { + setText(AnnotatedString(text)) + showToast(MR.strings.copied.localized()) +} + +actual fun shareFile(text: String, fileSource: CryptoFile) { + withApi { + FileChooserLauncher(false) { to: URI? -> + if (to != null) { + if (fileSource.cryptoArgs != null) { + decryptCryptoFile(getAppFilePath(fileSource.filePath), fileSource.cryptoArgs, to.path) + } else { + copyFileToFile(File(fileSource.filePath), to) {} + } + } + }.launch(fileSource.filePath) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt new file mode 100644 index 0000000000..1ea5018fcc --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/UI.desktop.kt @@ -0,0 +1,19 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.* +import chat.simplex.common.simplexWindowState +import chat.simplex.common.views.helpers.KeyboardState + +actual fun showToast(text: String, timeout: Long) { + simplexWindowState.toasts.add(text to timeout) +} + +@Composable +actual fun LockToCurrentOrientationUntilDispose() {} + +@Composable +actual fun LocalMultiplatformView(): Any? = null + +@Composable +actual fun getKeyboardState(): State<KeyboardState> = remember { mutableStateOf(KeyboardState.Opened) } +actual fun hideKeyboard(view: Any?) {} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt new file mode 100644 index 0000000000..e590c2e204 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/VideoPlayer.desktop.kt @@ -0,0 +1,216 @@ +package chat.simplex.common.platform + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import kotlinx.coroutines.* +import uk.co.caprica.vlcj.player.base.MediaPlayer +import uk.co.caprica.vlcj.player.component.CallbackMediaPlayerComponent +import uk.co.caprica.vlcj.player.component.EmbeddedMediaPlayerComponent +import java.awt.Component +import java.io.File +import java.net.URI +import kotlin.math.max + +actual class VideoPlayer actual constructor( + override val uri: URI, + override val gallery: Boolean, + private val defaultPreview: ImageBitmap, + defaultDuration: Long, + soundEnabled: Boolean +): VideoPlayerInterface { + override val soundEnabled: MutableState<Boolean> = mutableStateOf(false) + override val brokenVideo: MutableState<Boolean> = mutableStateOf(false) + override val videoPlaying: MutableState<Boolean> = mutableStateOf(false) + override val progress: MutableState<Long> = mutableStateOf(0L) + override val duration: MutableState<Long> = mutableStateOf(0L) + override val preview: MutableState<ImageBitmap> = mutableStateOf(defaultPreview) + + val mediaPlayerComponent = initializeMediaPlayerComponent() + val player by lazy { mediaPlayerComponent.mediaPlayer() } + + init { + withBGApi { + setPreviewAndDuration() + } + } + + private val currentVolume: Int by lazy { player.audio().volume() } + private var isReleased: Boolean = false + + private val listener: MutableState<((position: Long?, state: TrackState) -> Unit)?> = mutableStateOf(null) + private var progressJob: Job? = null + + enum class TrackState { + PLAYING, PAUSED, STOPPED + } + + private fun start(seek: Long? = null, onProgressUpdate: (position: Long?, state: TrackState) -> Unit): Boolean { + val filepath = getAppFilePath(uri) + if (filepath == null || !File(filepath).exists()) { + Log.e(TAG, "No such file: $uri") + brokenVideo.value = true + return false + } + + if (soundEnabled.value) { + RecorderInterface.stopRecording?.invoke() + } + AudioPlayer.stop() + VideoPlayerHolder.stopAll() + val playerFilePath = uri.toString().replaceFirst("file:", "file://") + if (listener.value == null) { + runCatching { + player.media().prepare(playerFilePath) + if (seek != null) { + player.seekTo(seek.toInt()) + } + }.onFailure { + Log.e(TAG, it.stackTraceToString()) + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error), it.message) + brokenVideo.value = true + return false + } + } + player.start() + if (seek != null) player.seekTo(seek.toInt()) + if (!player.isPlaying) { + // Can happen when video file is broken + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.unknown_error)) + brokenVideo.value = true + return false + } + listener.value = onProgressUpdate + // Player can only be accessed in one specific thread + progressJob = CoroutineScope(Dispatchers.Main).launch { + onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING) + while (isActive && !isReleased && player.isPlaying) { + // Even when current position is equal to duration, the player has isPlaying == true for some time, + // so help to make the playback stopped in UI immediately + if (player.currentPosition == player.duration) { + onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING) + break + } + delay(50) + onProgressUpdate(player.currentPosition.toLong(), TrackState.PLAYING) + } + if (isActive && !isReleased) { + onProgressUpdate(player.currentPosition.toLong(), TrackState.PAUSED) + } + onProgressUpdate(null, TrackState.PAUSED) + } + + return true + } + + override fun stop() { + if (isReleased || !videoPlaying.value) return + player.controls().stop() + stopListener() + } + + private fun stopListener() { + val afterCoroutineCancel: CompletionHandler = { + // Notify prev video listener about stop + listener.value?.invoke(null, TrackState.STOPPED) + } + /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: + * [TrackState.PLAYING] -> [TrackState.PAUSED] -> [TrackState.STOPPED] (in this order) + * */ + if (progressJob != null) { + progressJob?.invokeOnCompletion(afterCoroutineCancel) + } else { + afterCoroutineCancel(null) + } + progressJob?.cancel() + progressJob = null + } + + override fun play(resetOnEnd: Boolean) { + if (progress.value == duration.value) { + progress.value = 0 + } + videoPlaying.value = start(progress.value) { pro, _ -> + if (pro != null) { + progress.value = pro + } + if ((pro == null || pro == duration.value) && duration.value != 0L) { + videoPlaying.value = false + if (pro == duration.value) { + progress.value = if (resetOnEnd) 0 else duration.value + }/* else if (state == TrackState.STOPPED) { + progress.value = 0 // + }*/ + } + } + } + + override fun enableSound(enable: Boolean): Boolean { + if (isReleased) return false + if (soundEnabled.value == enable) return false + soundEnabled.value = enable + player.audio().setVolume(if (enable) currentVolume else 0) + return true + } + + override fun release(remove: Boolean) { withApi { + if (isReleased) return@withApi + isReleased = true + // TODO + /** [player.release] freezes thread for some reason. It happens periodically. So doing this we don't see the freeze, but it's still there */ + if (player.isPlaying) player.stop() + CoroutineScope(Dispatchers.IO).launch { player.release() } + if (remove) { + VideoPlayerHolder.players.remove(uri to gallery) + } + }} + + private val MediaPlayer.currentPosition: Int + get() = if (isReleased) 0 else max(0, player.status().time().toInt()) + + private suspend fun setPreviewAndDuration() { + // It freezes main thread, doing it in IO thread + CoroutineScope(Dispatchers.IO).launch { + val previewAndDuration = VideoPlayerHolder.previewsAndDurations.getOrPut(uri) { getBitmapFromVideo(defaultPreview, uri) } + withContext(Dispatchers.Main) { + preview.value = previewAndDuration.preview ?: defaultPreview + duration.value = (previewAndDuration.duration ?: 0) + } + } + } + + private fun initializeMediaPlayerComponent(): Component { + return if (desktopPlatform.isMac()) { + CallbackMediaPlayerComponent() + } else { + EmbeddedMediaPlayerComponent() + } + } + + private fun Component.mediaPlayer() = when (this) { + is CallbackMediaPlayerComponent -> mediaPlayer() + is EmbeddedMediaPlayerComponent -> mediaPlayer() + else -> error("mediaPlayer() can only be called on vlcj player components") + } + + companion object { + suspend fun getBitmapFromVideo(defaultPreview: ImageBitmap?, uri: URI?): VideoPlayerInterface.PreviewAndDuration { + val player = CallbackMediaPlayerComponent().mediaPlayer() + if (uri == null || !File(uri.rawPath).exists()) { + return VideoPlayerInterface.PreviewAndDuration(preview = defaultPreview, timestamp = 0L, duration = 0L) + } + player.media().startPaused(uri.toString().replaceFirst("file:", "file://")) + val start = System.currentTimeMillis() + while (player.snapshots()?.get() == null && start + 5000 > System.currentTimeMillis()) { + delay(10) + } + val preview = player.snapshots()?.get()?.toComposeImageBitmap() + val duration = player.duration.toLong() + CoroutineScope(Dispatchers.IO).launch { player.release() } + return VideoPlayerInterface.PreviewAndDuration(preview = preview, timestamp = 0L, duration = duration) + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt new file mode 100644 index 0000000000..54a511e082 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Videos.desktop.kt @@ -0,0 +1,13 @@ +package chat.simplex.common.platform + +import java.net.URI + +fun isVideo(uri: URI): Boolean { + val path = uri.path.lowercase() + return path.endsWith(".mov") || + path.endsWith(".avi") || + path.endsWith(".mp4") || + path.endsWith(".mpg") || + path.endsWith(".mpeg") || + path.endsWith(".mkv") +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt new file mode 100644 index 0000000000..94268002a6 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt @@ -0,0 +1,19 @@ +package chat.simplex.common.ui.theme + +import chat.simplex.common.platform.Log +import chat.simplex.common.platform.TAG +import com.jthemedetecor.OsThemeDetector + +private val detector: OsThemeDetector = OsThemeDetector.getDetector() + .apply { + registerListener(::reactOnDarkThemeChanges) + } + +actual fun isSystemInDarkTheme(): Boolean = try { + detector.isDark +} +catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + /* On Mac this code can produce exception */ + false +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Type.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Type.desktop.kt new file mode 100644 index 0000000000..082036558c --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Type.desktop.kt @@ -0,0 +1,28 @@ +package chat.simplex.common.ui.theme + +import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.platform.Font +import chat.simplex.common.platform.desktopPlatform +import chat.simplex.res.MR + +actual val Inter: FontFamily = FontFamily( + Font(MR.fonts.Inter.regular.file), + Font(MR.fonts.Inter.italic.file, style = FontStyle.Italic), + Font(MR.fonts.Inter.bold.file, FontWeight.Bold), + Font(MR.fonts.Inter.semibold.file, FontWeight.SemiBold), + Font(MR.fonts.Inter.medium.file, FontWeight.Medium), + Font(MR.fonts.Inter.light.file, FontWeight.Light) +) + +actual val EmojiFont: FontFamily = if (desktopPlatform.isMac()) { + FontFamily.Default +} else { + FontFamily( + Font(MR.fonts.NotoColorEmoji.regular.file), + Font(MR.fonts.NotoColorEmoji.regular.file, style = FontStyle.Italic), + Font(MR.fonts.NotoColorEmoji.regular.file, FontWeight.Bold), + Font(MR.fonts.NotoColorEmoji.regular.file, FontWeight.SemiBold), + Font(MR.fonts.NotoColorEmoji.regular.file, FontWeight.Medium), + Font(MR.fonts.NotoColorEmoji.regular.file, FontWeight.Light) + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt new file mode 100644 index 0000000000..df45a8acb0 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -0,0 +1,8 @@ +package chat.simplex.common.views.call + +import androidx.compose.runtime.Composable + +@Composable +actual fun ActiveCallView() { + // LALAL +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeView.desktop.kt new file mode 100644 index 0000000000..dd9f3dd612 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ComposeView.desktop.kt @@ -0,0 +1,40 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* +import java.net.URI + +@Composable +actual fun AttachmentSelection( + composeState: MutableState<ComposeState>, + attachmentOption: MutableState<AttachmentOption?>, + processPickedFile: (URI?, String?) -> Unit, + processPickedMedia: (List<URI>, String?) -> Unit +) { + val imageLauncher = rememberFileChooserMultipleLauncher { + processPickedMedia(it, null) + } + val videoLauncher = rememberFileChooserMultipleLauncher { + processPickedMedia(it, null) + } + val filesLauncher = rememberFileChooserLauncher(true) { + if (it != null) processPickedFile(it, null) + } + LaunchedEffect(attachmentOption.value) { + when (attachmentOption.value) { + AttachmentOption.CameraPhoto -> {} + AttachmentOption.GalleryImage -> { + imageLauncher.launch("image/*") + } + AttachmentOption.GalleryVideo -> { + videoLauncher.launch("video/*") + } + AttachmentOption.File -> { + filesLauncher.launch("*/*") + } + else -> {} + } + attachmentOption.value = null + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.desktop.kt new file mode 100644 index 0000000000..7ea2ef5368 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/ScanCodeView.desktop.kt @@ -0,0 +1,8 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.Composable + +@Composable +actual fun ScanCodeView(verifyCode: (String?, cb: (Boolean) -> Unit) -> Unit, close: () -> Unit) { + ScanCodeLayout(verifyCode, close) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/SendMsgView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/SendMsgView.desktop.kt new file mode 100644 index 0000000000..05ff1d73dc --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/SendMsgView.desktop.kt @@ -0,0 +1,11 @@ +package chat.simplex.common.views.chat + +import androidx.compose.runtime.Composable + +@Composable +actual fun allowedToRecordVoiceByPlatform(): Boolean = true + +@Composable +actual fun VoiceButtonWithoutPermissionByPlatform() { + VoiceButtonWithoutPermission {} +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt new file mode 100644 index 0000000000..711e09267d --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt @@ -0,0 +1,28 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter +import chat.simplex.common.model.CIFile +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.ModalManager +import java.net.URI + +@Composable +actual fun SimpleAndAnimatedImageView( + data: ByteArray, + imageBitmap: ImageBitmap, + file: CIFile?, + imageProvider: () -> ImageGalleryProvider, + ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit +) { + // LALAL make it animated too + ImageView(imageBitmap.toAwtImage().toPainter()) { + if (getLoadedFilePath(file) != null) { + ModalManager.fullscreen.showCustomModal(animated = false) { close -> + ImageFullScreenView(imageProvider, close) + } + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt new file mode 100644 index 0000000000..c85057b47e --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.desktop.kt @@ -0,0 +1,21 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import chat.simplex.common.platform.VideoPlayer + +@Composable +actual fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) {} + +@Composable +actual fun LocalWindowWidth(): Dp { + return with(LocalDensity.current) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } + /*val density = LocalDensity.current + var width by remember { mutableStateOf(with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) } + SideEffect { + if (width != with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() }) + width = with(density) { (java.awt.Window.getWindows().find { it.isActive }?.width ?: 0).toDp() } + } + return width.also { println("LALAL $it") }*/ +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt new file mode 100644 index 0000000000..c1d9eeec52 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.desktop.kt @@ -0,0 +1,41 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import chat.simplex.common.model.ChatItem +import chat.simplex.common.model.MsgContent +import chat.simplex.common.platform.FileChooserLauncher +import chat.simplex.common.platform.desktopPlatform +import chat.simplex.common.ui.theme.EmojiFont +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun ReactionIcon(text: String, fontSize: TextUnit) { + if (desktopPlatform.isMac() && isHeartEmoji(text)) { + val sp = with(LocalDensity.current) { (fontSize.value + 8).sp.toDp() } + Image(painterResource(MR.images.ic_heart), null, Modifier.size(sp).padding(top = 4.dp, bottom = 2.dp)) + } else { + Text(text, fontSize = fontSize, fontFamily = EmojiFont) + } +} + +@Composable +actual fun SaveContentItemAction(cItem: ChatItem, saveFileLauncher: FileChooserLauncher, showMenu: MutableState<Boolean>) { + ItemAction(stringResource(MR.strings.save_verb), painterResource(if (cItem.file?.fileSource?.cryptoArgs == null) MR.images.ic_download else MR.images.ic_lock_open_right), onClick = { + when (cItem.content.msgContent) { + is MsgContent.MCImage, is MsgContent.MCFile, is MsgContent.MCVoice, is MsgContent.MCVideo -> withApi { saveFileLauncher.launch(cItem.file?.fileName ?: "") } + else -> {} + } + showMenu.value = false + }) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.desktop.kt new file mode 100644 index 0000000000..05ed7002e5 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.desktop.kt @@ -0,0 +1,27 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.MREmojiChar +import chat.simplex.common.platform.desktopPlatform +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +actual fun EmojiText(text: String) { + val s = text.trim() + if (desktopPlatform.isMac() && isHeartEmoji(s)) { + Image(painterResource(MR.images.ic_heart), null, Modifier.height(62.dp).width(54.dp).padding(vertical = 8.dp)) + } else { + Text(s, style = if (s.codePoints().count() < 4) largeEmojiFont else mediumEmojiFont) + } +} + +/** [MREmojiChar.Heart.value] */ +fun isHeartEmoji(s: String): Boolean = + (s.codePoints().count() == 2L && s.codePointAt(0) == 10084 && s.codePointAt(1) == 65039) || + s.codePoints().count() == 1L && s.codePointAt(0) == 10084 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt new file mode 100644 index 0000000000..9aafc83d23 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.desktop.kt @@ -0,0 +1,71 @@ +package chat.simplex.common.views.chat.item + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel +import androidx.compose.ui.graphics.* +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import chat.simplex.common.views.helpers.getBitmapFromByteArray +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay +import kotlin.math.max + +@Composable +actual fun FullScreenImageView(modifier: Modifier, data: ByteArray, imageBitmap: ImageBitmap) { + Image( + getBitmapFromByteArray(data, false) ?: MR.images.decentralized.image.toComposeImageBitmap(), + contentDescription = stringResource(MR.strings.image_descr), + contentScale = ContentScale.Fit, + modifier = modifier, + ) +} +@Composable +actual fun FullScreenVideoView(player: VideoPlayer, modifier: Modifier, close: () -> Unit) { + // Workaround. Without changing size of the window the screen flashes a lot even if it's not being recomposed + LaunchedEffect(Unit) { + simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width + 1.dp) + delay(50) + player.play(true) + simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = simplexWindowState.windowState.size.width - 1.dp) + } + Box { + Box(Modifier.fillMaxSize().padding(bottom = 50.dp)) { + val factory = remember { { player.mediaPlayerComponent } } + SwingPanel( + background = Color.Transparent, + modifier = Modifier, + factory = factory + ) + } + Controls(player, close) + } +} + +@Composable +private fun BoxScope.Controls(player: VideoPlayer, close: () -> Unit) { + val playing = remember(player) { player.videoPlaying } + val progress = remember(player) { player.progress } + val duration = remember(player) { player.duration } + Row(Modifier.fillMaxWidth().align(Alignment.BottomCenter).height(50.dp)) { + IconButton(onClick = { if (playing.value) player.player.pause() else player.play(true) },) { + Icon(painterResource(if (playing.value) MR.images.ic_pause_filled else MR.images.ic_play_arrow_filled), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary) + } + Slider( + value = progress.value.toFloat() / max(0.0001f, duration.value.toFloat()), + onValueChange = { player.player.seekTo((it * duration.value).toInt()) }, + modifier = Modifier.fillMaxWidth().weight(1f) + ) + IconButton(onClick = close,) { + Icon(painterResource(MR.images.ic_close), null, Modifier.size(30.dp), tint = MaterialTheme.colors.primary) + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt new file mode 100644 index 0000000000..af2b269b58 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.desktop.kt @@ -0,0 +1,106 @@ +package chat.simplex.common.views.database + +import SectionItemView +import SectionTextFooter +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.WarningOrange +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun SavePassphraseSetting( + useKeychain: Boolean, + initialRandomDBPassphrase: Boolean, + storedKey: Boolean, + progressIndicator: Boolean, + minHeight: Dp, + onCheckedChange: (Boolean) -> Unit, +) { + SectionItemView(minHeight = minHeight) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled), + stringResource(MR.strings.save_passphrase_in_settings), + tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary + ) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + stringResource(MR.strings.save_passphrase_in_settings), + Modifier.padding(end = 24.dp), + color = Color.Unspecified + ) + Spacer(Modifier.fillMaxWidth().weight(1f)) + DefaultSwitch( + checked = useKeychain, + onCheckedChange = onCheckedChange, + enabled = !initialRandomDBPassphrase && !progressIndicator + ) + } + } +} + +@Composable +actual fun DatabaseEncryptionFooter( + useKeychain: MutableState<Boolean>, + chatDbEncrypted: Boolean?, + storedKey: MutableState<Boolean>, + initialRandomDBPassphrase: MutableState<Boolean>, +) { + if (chatDbEncrypted == false) { + SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted)) + } else if (useKeychain.value) { + if (storedKey.value) { + SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text)) + if (initialRandomDBPassphrase.value) { + SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase)) + } else { + SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) + } + } else { + SectionTextFooter(generalGetString(MR.strings.passphrase_will_be_saved_in_settings)) + } + } else { + SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) + SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) + } +} + +actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.encrypt_database_question), + text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored_in_settings) + "\n" + storeSecurelySaved(), + confirmText = generalGetString(MR.strings.encrypt_database), + onConfirm = onConfirm, + destructive = true, + ) +} + +actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.change_database_passphrase_question), + text = generalGetString(MR.strings.database_encryption_will_be_updated_in_settings) + "\n" + storeSecurelySaved(), + confirmText = generalGetString(MR.strings.update_database), + onConfirm = onConfirm, + destructive = false, + ) +} + +actual fun removePassphraseAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.remove_passphrase_from_settings), + text = storeSecurelyDanger(), + confirmText = generalGetString(MR.strings.remove_passphrase), + onConfirm = onConfirm, + destructive = true, + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt new file mode 100644 index 0000000000..d125f94e55 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/ChooseAttachmentView.desktop.kt @@ -0,0 +1,26 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.ui.Modifier +import chat.simplex.common.views.newchat.ActionButton +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun ChooseAttachmentButtons(attachmentOption: MutableState<AttachmentOption?>, hide: () -> Unit) { + ActionButton(Modifier.fillMaxWidth(0.33f), null, stringResource(MR.strings.gallery_image_button), icon = painterResource(MR.images.ic_add_photo)) { + attachmentOption.value = AttachmentOption.GalleryImage + hide() + } + ActionButton(Modifier.fillMaxWidth(0.5f), null, stringResource(MR.strings.gallery_video_button), icon = painterResource(MR.images.ic_smart_display)) { + attachmentOption.value = AttachmentOption.GalleryVideo + hide() + } + ActionButton(Modifier.fillMaxWidth(1f), null, stringResource(MR.strings.choose_file), icon = painterResource(MR.images.ic_note_add)) { + attachmentOption.value = AttachmentOption.File + hide() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt new file mode 100644 index 0000000000..20675dc74e --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDialog.desktop.kt @@ -0,0 +1,163 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.* +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.* +import chat.simplex.common.DialogParams +import chat.simplex.res.MR +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.awt.FileDialog +import java.io.File +import java.util.* +import javax.swing.JFileChooser +import javax.swing.filechooser.FileFilter +import javax.swing.filechooser.FileNameExtensionFilter + +@Composable +actual fun DefaultDialog( + onDismissRequest: () -> Unit, + content: @Composable () -> Unit +) { + Dialog( + undecorated = true, + transparent = true, + resizable = false, + title = "", + onCloseRequest = onDismissRequest, + onPreviewKeyEvent = { event -> + if (event.key == Key.Escape && event.type == KeyEventType.KeyUp) { + onDismissRequest(); true + } else false + } + ) { + Surface( + Modifier + .border(border = BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.3F)), shape = RoundedCornerShape(8)) + ) { + content() + } + } +} + +@Composable +fun FrameWindowScope.FileDialogChooser( + title: String, + isLoad: Boolean, + params: DialogParams, + onResult: (result: List<File>) -> Unit +) { + if (isLinux()) { + FileDialogChooserMultiple(title, isLoad, params.filename, params.allowMultiple, params.fileFilter, params.fileFilterDescription, onResult) + } else { + FileDialogAwt(title, isLoad, params.filename, params.allowMultiple, params.fileFilter, onResult) + } +} + +@Composable +fun FrameWindowScope.FileDialogChooserMultiple( + title: String, + isLoad: Boolean, + filename: String?, + allowMultiple: Boolean, + fileFilter: ((File?) -> Boolean)? = null, + fileFilterDescription: String? = null, + onResult: (result: List<File>) -> Unit +) { + val scope = rememberCoroutineScope() + DisposableEffect(Unit) { + val job = scope.launch(Dispatchers.Main) { + val fileChooser = JFileChooser() + fileChooser.dialogTitle = title + fileChooser.isMultiSelectionEnabled = allowMultiple && isLoad + fileChooser.isAcceptAllFileFilterUsed = fileFilter == null + if (fileFilter != null && fileFilterDescription != null) { + fileChooser.addChoosableFileFilter(object: FileFilter() { + override fun accept(file: File?): Boolean = fileFilter(file) + + override fun getDescription(): String = fileFilterDescription + }) + } + val returned = if (isLoad) { + fileChooser.showOpenDialog(window) + } else { + if (filename != null) { + fileChooser.selectedFile = File(filename) + } else { + fileChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + } + fileChooser.showSaveDialog(window) + } + val result = when (returned) { + JFileChooser.APPROVE_OPTION -> { + if (isLoad) { + when { + allowMultiple -> fileChooser.selectedFiles.filter { it.canRead() } + fileChooser.selectedFile != null && fileChooser.selectedFile.canRead() -> listOf(fileChooser.selectedFile) + else -> emptyList() + } + } else { + if (!fileChooser.fileFilter.accept(fileChooser.selectedFile)) { + val ext = (fileChooser.fileFilter as FileNameExtensionFilter).extensions[0] + fileChooser.selectedFile = File(fileChooser.selectedFile.absolutePath + ".$ext") + } + listOf(fileChooser.selectedFile) + } + } + else -> emptyList() + } + onResult(result) + } + onDispose { + job.cancel() + } + } +} + +/* +* Has graphic glitches on many Linux distributions, so use only on non-Linux systems +* */ +@Composable +private fun FrameWindowScope.FileDialogAwt( + title: String, + isLoad: Boolean, + filename: String?, + allowMultiple: Boolean, + fileFilter: ((File?) -> Boolean)? = null, + onResult: (result: List<File>) -> Unit +) = AwtWindow( + create = { + object: FileDialog(window, generalGetString(MR.strings.choose_file_title), if (isLoad) LOAD else SAVE) { + override fun setVisible(value: Boolean) { + super.setVisible(value) + if (value) { + if (files != null) { + onResult(files.toList()) + } else { + onResult(emptyList()) + } + } + } + }.apply { + this.title = title + this.isMultipleMode = allowMultiple && isLoad + if (!isLoad && filename != null) { + this.file = filename + } + if (fileFilter != null) { + this.setFilenameFilter { dir, file -> + fileFilter(File(dir.absolutePath + File.separator + file)) + } + } + } + }, + dispose = FileDialog::dispose +) + +fun isLinux(): Boolean = System.getProperty("os.name", "generic").lowercase(Locale.ENGLISH) == "linux" diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDropDownMenu.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDropDownMenu.desktop.kt new file mode 100644 index 0000000000..5dfd44ba6b --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/DefaultDropDownMenu.desktop.kt @@ -0,0 +1,44 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material.DropdownMenu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() } + +actual interface DefaultExposedDropdownMenuBoxScope { + @Composable + actual fun DefaultExposedDropdownMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier, + content: @Composable ColumnScope.() -> Unit + ) { + DropdownMenu(expanded, onDismissRequest, offset = DpOffset(0.dp, (-40).dp)) { + Column { + content() + } + } + } +} + +@Composable +actual fun DefaultExposedDropdownMenuBox( + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + modifier: Modifier, + content: @Composable DefaultExposedDropdownMenuBoxScope.() -> Unit +) { + val obj = remember { object : DefaultExposedDropdownMenuBoxScope {} } + Box(Modifier + .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { onExpandedChange(!expanded) }) + ) { + obj.content() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt new file mode 100644 index 0000000000..979857f044 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/GetImageView.desktop.kt @@ -0,0 +1,52 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.ImageBitmap +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.rememberFileChooserLauncher +import chat.simplex.res.MR +import chat.simplex.common.views.newchat.ActionButton +import java.net.URI + +@Composable +actual fun GetImageBottomSheet( + imageBitmap: MutableState<URI?>, + onImageChange: (ImageBitmap) -> Unit, + hideBottomSheet: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .onFocusChanged { focusState -> + if (!focusState.hasFocus) hideBottomSheet() + } + ) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 30.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + val processPickedImage = { uri: URI? -> + if (uri != null) { + val bitmap = getBitmapFromUri(uri) + if (bitmap != null) { + imageBitmap.value = uri + onImageChange(bitmap) + } + hideBottomSheet() + } + } + val pickImageLauncher = rememberFileChooserLauncher(true, null, processPickedImage) + ActionButton(null, stringResource(MR.strings.from_gallery_button), icon = painterResource(MR.images.ic_image)) { + withApi { pickImageLauncher.launch("image/*") } + } + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt new file mode 100644 index 0000000000..a251b7dc20 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/LocalAuthentication.desktop.kt @@ -0,0 +1,16 @@ +package chat.simplex.common.views.helpers + +import chat.simplex.common.views.usersettings.LAMode + +actual fun authenticate( + promptTitle: String, + promptSubtitle: String, + selfDestruct: Boolean, + usingLAMode: LAMode, + completed: (LAResult) -> Unit +) { + when (usingLAMode) { + LAMode.PASSCODE -> authenticateWithPasscode(promptTitle, promptSubtitle, selfDestruct, completed) + else -> {} + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt new file mode 100644 index 0000000000..21b2cfa6e0 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/Utils.desktop.kt @@ -0,0 +1,143 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.ui.graphics.* +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Density +import chat.simplex.common.model.CIFile +import chat.simplex.common.model.readCryptoFile +import chat.simplex.common.platform.* +import chat.simplex.common.simplexWindowState +import java.io.ByteArrayInputStream +import java.io.File +import java.net.URI +import javax.imageio.ImageIO +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi +import kotlin.io.path.toPath + +private val bStyle = SpanStyle(fontWeight = FontWeight.Bold) +private val iStyle = SpanStyle(fontStyle = FontStyle.Italic) +private fun fontStyle(color: String) = + SpanStyle(color = Color(color.replace("#", "ff").toLongOrNull(16) ?: Color.White.toArgb().toLong())) + +actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString = try { + buildAnnotatedString { + fun String.substringSafe(start: Int, len: Int): String = + if (start < 0 || start >= this.length || start + len < 0 || start + len > this.length) "" + else substring(start, start + len) + + var skipTil = 0 + for (outerI in text.indices) { + if (skipTil > outerI) continue + if (text[outerI] == '<') { + for (innerI in outerI + 1 until text.length) { + when { + text.substringSafe(innerI, 2) == "b>" -> { + val textStart = innerI + 2 + for (insideTagI in textStart until text.length) { + if (text[insideTagI] == '<') { + withStyle(bStyle) { append(text.substring(textStart, insideTagI)) } + skipTil = insideTagI + 4 + break + } + } + break + } + text.substringSafe(innerI, 2) == "i>" -> { + val textStart = innerI + 2 + for (insideTagI in textStart until text.length) { + if (text[insideTagI] == '<') { + withStyle(iStyle) { append(text.substring(textStart, insideTagI)) } + skipTil = insideTagI + 4 + break + } + } + break + } + text.substringSafe(innerI, 4) == "font" -> { + var textStart = innerI + 5 + var color = "#000000" + for (i in textStart until text.length) { + if (text[i] == '#') { + color = text.substring(i, i + 7) + textStart = i + 9 + break + } + } + for (insideTagI in textStart until text.length) { + if (text[insideTagI] == '<') { + withStyle(fontStyle(color)) { append(text.substring(textStart, insideTagI)) } + skipTil = insideTagI + 7 + break + } + } + break + } + } + if (skipTil > innerI) continue + } + } else { + append(text[outerI]) + } + } + } +} catch (e: Exception) { + AnnotatedString(text) +} + +actual fun getAppFileUri(fileName: String): URI = + URI("file:" + appFilesDir.absolutePath + File.separator + fileName) + +actual fun getLoadedImage(file: CIFile?): Pair<ImageBitmap, ByteArray>? { + val filePath = getLoadedFilePath(file) + return if (filePath != null) { + val data = if (file?.fileSource?.cryptoArgs != null) readCryptoFile(filePath, file.fileSource.cryptoArgs) else File(filePath).readBytes() + val bitmap = getBitmapFromByteArray(data, false) + if (bitmap != null) bitmap to data else null + } else { + null + } +} + +actual fun getFileName(uri: URI): String? = uri.toPath().toFile().name + +actual fun getAppFilePath(uri: URI): String? = uri.path + +actual fun getFileSize(uri: URI): Long? = uri.toPath().toFile().length() + +actual fun getBitmapFromUri(uri: URI, withAlertOnException: Boolean): ImageBitmap? = + ImageIO.read(uri.inputStream()).toComposeImageBitmap() + +actual fun getBitmapFromByteArray(data: ByteArray, withAlertOnException: Boolean): ImageBitmap? = + ImageIO.read(ByteArrayInputStream(data)).toComposeImageBitmap() + +// LALAL implement to support animated drawable +actual fun getDrawableFromUri(uri: URI, withAlertOnException: Boolean): Any? = null + +actual suspend fun saveTempImageUncompressed(image: ImageBitmap, asPng: Boolean): File? { + val file = simplexWindowState.saveDialog.awaitResult() + return if (file != null) { + try { + val ext = if (asPng) "png" else "jpg" + val newFile = File(file.absolutePath + File.separator + generateNewFileName("IMG", ext)) + // LALAL FILE IS EMPTY + ImageIO.write(image.toAwtImage(), ext.uppercase(), newFile.outputStream()) + newFile + } catch (e: Exception) { + Log.e(TAG, "Util.kt saveTempImageUncompressed error: ${e.message}") + null + } + } else null +} + +actual suspend fun getBitmapFromVideo(uri: URI, timestamp: Long?, random: Boolean): VideoPlayerInterface.PreviewAndDuration { + return VideoPlayer.getBitmapFromVideo(null, uri) +} + +@OptIn(ExperimentalEncodingApi::class) +actual fun ByteArray.toBase64StringForPassphrase(): String = Base64.encode(this) + +@OptIn(ExperimentalEncodingApi::class) +actual fun String.toByteArrayFromBase64ForPassphrase(): ByteArray = Base64.decode(this) diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.desktop.kt new file mode 100644 index 0000000000..84da0a7757 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ConnectViaLinkView.desktop.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.runtime.* +import chat.simplex.common.model.ChatModel + +@Composable +actual fun ConnectViaLinkView(m: ChatModel, close: () -> Unit) { + PasteToConnectView(m, close) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCode.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCode.desktop.kt new file mode 100644 index 0000000000..ae5cd24ce7 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCode.desktop.kt @@ -0,0 +1,18 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.ui.graphics.* + +actual fun ImageBitmap.replaceColor(from: Int, to: Int): ImageBitmap { + val image = toAwtImage() + val pixels = IntArray(width * height) + image.getRGB(0, 0, width, height, pixels, 0, image.width) + var i = 0 + while (i < pixels.size) { + if (pixels[i] == from) { + pixels[i] = to + } + i++ + } + image.setRGB(0, 0, width, height, pixels, 0, image.width) + return image.toComposeImageBitmap() +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt new file mode 100644 index 0000000000..16d35b5b8d --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/QRCodeScanner.desktop.kt @@ -0,0 +1,8 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.runtime.* + +@Composable +actual fun QRCodeScanner(onBarcode: (String) -> Unit) { + //LALAL +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.desktop.kt new file mode 100644 index 0000000000..f202318f16 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.desktop.kt @@ -0,0 +1,13 @@ +package chat.simplex.common.views.newchat + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.ChatModel + +@Composable +actual fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) { + ConnectContactLayout( + chatModel = chatModel, + incognitoPref = chatModel.controller.appPrefs.incognito, + close = close + ) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.desktop.kt new file mode 100644 index 0000000000..f770de904a --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.desktop.kt @@ -0,0 +1,6 @@ +package chat.simplex.common.views.onboarding + +import androidx.compose.runtime.Composable + +@Composable +actual fun SetNotificationsModeAdditions() {} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt new file mode 100644 index 0000000000..1a8ad010e3 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -0,0 +1,70 @@ +package chat.simplex.common.views.usersettings + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionView +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.platform.defaultLocale +import chat.simplex.common.ui.theme.ThemeColor +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.delay +import java.util.Locale + +@Composable +actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { + AppearanceScope.AppearanceLayout( + m.controller.appPrefs.appLanguage, + m.controller.appPrefs.systemDarkTheme, + showSettingsModal = showSettingsModal, + editColor = { name, initialColor -> + ModalManager.start.showModalCloseable { close -> + ColorEditor(name, initialColor, close) + } + }, + ) +} + +@Composable +fun AppearanceScope.AppearanceLayout( + languagePref: SharedPreference<String?>, + systemDarkTheme: SharedPreference<String?>, + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + editColor: (ThemeColor, Color) -> Unit, +) { + Column( + Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), + ) { + AppBarTitle(stringResource(MR.strings.appearance_settings)) + SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { + val state = rememberSaveable { mutableStateOf(languagePref.get() ?: "system") } + LangSelector(state) { + state.value = it + withApi { + delay(200) + if (it == "system") { + languagePref.set(null) + Locale.setDefault(defaultLocale) + } else { + languagePref.set(it) + Locale.setDefault(Locale.forLanguageTag(it)) + } + } + } + } + SectionDividerSpaced(maxTopPadding = true) + ThemesSection(systemDarkTheme, showSettingsModal, editColor) + SectionBottomSpacer() + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.desktop.kt new file mode 100644 index 0000000000..a51fd20c5b --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.desktop.kt @@ -0,0 +1,17 @@ +package chat.simplex.common.views.usersettings + +import SectionView +import androidx.compose.runtime.Composable +import chat.simplex.common.model.ChatModel +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun PrivacyDeviceSection( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + setPerformLA: (Boolean) -> Unit, +) { + SectionView(stringResource(MR.strings.settings_section_title_device)) { + ChatLockItem(showSettingsModal, setPerformLA) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt new file mode 100644 index 0000000000..464c286316 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.desktop.kt @@ -0,0 +1,9 @@ +package chat.simplex.common.views.usersettings + +import androidx.compose.runtime.Composable +import chat.simplex.common.model.ServerCfg + +@Composable +actual fun ScanProtocolServer(onNext: (ServerCfg) -> Unit) { + ScanProtocolServerLayout(onNext) +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt new file mode 100644 index 0000000000..95a079fe1b --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -0,0 +1,21 @@ +package chat.simplex.common.views.usersettings + +import SectionView +import androidx.compose.runtime.Composable +import chat.simplex.common.model.ChatModel +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource + +@Composable +actual fun SettingsSectionApp( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showCustomModal: (@Composable (ChatModel, () -> Unit) -> Unit) -> (() -> Unit), + showVersion: () -> Unit, + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit +) { + SectionView(stringResource(MR.strings.settings_section_title_app)) { + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) + AppVersionItem(showVersion) + } +} diff --git a/apps/multiplatform/desktop/build.gradle.kts b/apps/multiplatform/desktop/build.gradle.kts index 95a4908dbd..a4e8207a15 100644 --- a/apps/multiplatform/desktop/build.gradle.kts +++ b/apps/multiplatform/desktop/build.gradle.kts @@ -1,4 +1,5 @@ import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.util.capitalizeDecapitalize.toLowerCaseAsciiOnly plugins { kotlin("multiplatform") @@ -20,6 +21,7 @@ kotlin { dependencies { implementation(project(":common")) implementation(compose.desktop.currentOs) + implementation("net.java.dev.jna:jna:5.13.0") } } val jvmTest by getting @@ -30,9 +32,24 @@ kotlin { compose { desktop { application { - mainClass = "MainKt" + // For debugging via VisualVM + val debugJava = false + if (debugJava) { + jvmArgs += listOf( + "-Dcom.sun.management.jmxremote.port=8080", + "-Dcom.sun.management.jmxremote.ssl=false", + "-Dcom.sun.management.jmxremote.authenticate=false" + ) + } + mainClass = "chat.simplex.desktop.MainKt" nativeDistributions { - modules("jdk.zipfs") + // For debugging via VisualVM + if (debugJava) { + modules("jdk.zipfs", "jdk.unsupported", "jdk.management.agent") + } else { + // 'jdk.unsupported' is for vlcj + modules("jdk.zipfs", "jdk.unsupported") + } //includeAllModules = true outputBaseDir.set(project.file("../release")) targetFormats( @@ -40,23 +57,45 @@ compose { //, TargetFormat.AppImage // Gradle doesn't sync on Mac with it ) linux { - iconFile.set(project.file("../common/src/commonMain/resources/distribute/simplex.png")) + iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.png")) appCategory = "Messenger" } windows { - // LALAL - iconFile.set(project.file("../common/src/commonMain/resources/distribute/simplex.ico")) + packageName = "SimpleX" + iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.ico")) console = true perUserInstall = true dirChooser = true } macOS { - // LALAL - //iconFile.set(project.file("../common/src/commonMain/resources/distribute/simplex.icns")) + packageName = "SimpleX" + iconFile.set(project.file("src/jvmMain/resources/distribute/simplex.icns")) appCategory = "public.app-category.social-networking" bundleID = "chat.simplex.app" + val identity = rootProject.extra["desktop.mac.signing.identity"] as String? + val keychain = rootProject.extra["desktop.mac.signing.keychain"] as String? + val appleId = rootProject.extra["desktop.mac.notarization.apple_id"] as String? + val password = rootProject.extra["desktop.mac.notarization.password"] as String? + val teamId = rootProject.extra["desktop.mac.notarization.team_id"] as String? + if (identity != null && keychain != null && appleId != null && password != null) { + signing { + sign.set(true) + this.identity.set(identity) + this.keychain.set(keychain) + } + notarization { + this.appleID.set(appleId) + this.password.set(password) + this.ascProvider.set(teamId) + } + } + } + val os = System.getProperty("os.name", "generic").toLowerCaseAsciiOnly() + if (os.contains("mac") || os.contains("win")) { + packageName = "SimpleX" + } else { + packageName = "simplex" } - packageName = "simplex" // Packaging requires to have version like MAJOR.MINOR.PATCH var adjustedVersion = rootProject.extra["desktop.version_name"] as String adjustedVersion = adjustedVersion.replace(Regex("[^0-9.]"), "") @@ -113,30 +152,47 @@ tasks.named("compileJava") { afterEvaluate { tasks.create("cmakeBuildAndCopy") { dependsOn("cmakeBuild") + val copyDetails = mutableMapOf<String, ArrayList<FileCopyDetails>>() doLast { copy { from("${project(":desktop").buildDir}/cmake/main/linux-amd64", "$cppPath/desktop/libs/linux-x86_64", "$cppPath/desktop/libs/linux-x86_64/deps") - into("../common/src/commonMain/resources/libs/linux-x86_64") - include("*.so") + into("src/jvmMain/resources/libs/linux-x86_64") + include("*.so*") eachFile { path = name } includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } + copy { + val destinationDir = "src/jvmMain/resources/libs/linux-x86_64/vlc" + from("$cppPath/desktop/libs/linux-x86_64/deps/vlc") + into(destinationDir) + includeEmptyDirs = false + duplicatesStrategy = DuplicatesStrategy.INCLUDE + copyIfNeeded(destinationDir, copyDetails) + } copy { from("${project(":desktop").buildDir}/cmake/main/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64", "$cppPath/desktop/libs/linux-aarch64/deps") - into("../common/src/commonMain/resources/libs/linux-aarch64") - include("*.so") + into("src/jvmMain/resources/libs/linux-aarch64") + include("*.so*") eachFile { path = name } includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } + copy { + val destinationDir = "src/jvmMain/resources/libs/linux-aarch64/vlc" + from("$cppPath/desktop/libs/linux-aarch64/deps/vlc") + into(destinationDir) + includeEmptyDirs = false + duplicatesStrategy = DuplicatesStrategy.INCLUDE + copyIfNeeded(destinationDir, copyDetails) + } copy { from("${project(":desktop").buildDir}/cmake/main/win-amd64", "$cppPath/desktop/libs/windows-x86_64", "$cppPath/desktop/libs/windows-x86_64/deps") - into("../common/src/commonMain/resources/libs/windows-x86_64") + into("src/jvmMain/resources/libs/windows-x86_64") include("*.dll") eachFile { path = name @@ -144,9 +200,17 @@ afterEvaluate { includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } + copy { + val destinationDir = "src/jvmMain/resources/libs/windows-x86_64/vlc" + from("$cppPath/desktop/libs/windows-x86_64/deps/vlc") + into(destinationDir) + includeEmptyDirs = false + duplicatesStrategy = DuplicatesStrategy.INCLUDE + copyIfNeeded(destinationDir, copyDetails) + } copy { from("${project(":desktop").buildDir}/cmake/main/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64", "$cppPath/desktop/libs/mac-x86_64/deps") - into("../common/src/commonMain/resources/libs/mac-x86_64") + into("src/jvmMain/resources/libs/mac-x86_64") include("*.dylib") eachFile { path = name @@ -154,9 +218,17 @@ afterEvaluate { includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } + copy { + val destinationDir = "src/jvmMain/resources/libs/mac-x86_64/vlc" + from("$cppPath/desktop/libs/mac-x86_64/deps/vlc") + into(destinationDir) + includeEmptyDirs = false + duplicatesStrategy = DuplicatesStrategy.INCLUDE + copyIfNeeded(destinationDir, copyDetails) + } copy { from("${project(":desktop").buildDir}/cmake/main/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64", "$cppPath/desktop/libs/mac-aarch64/deps") - into("../common/src/commonMain/resources/libs/mac-aarch64") + into("src/jvmMain/resources/libs/mac-aarch64") include("*.dylib") eachFile { path = name @@ -164,6 +236,39 @@ afterEvaluate { includeEmptyDirs = false duplicatesStrategy = DuplicatesStrategy.INCLUDE } + copy { + val destinationDir = "src/jvmMain/resources/libs/mac-aarch64/vlc" + from("$cppPath/desktop/libs/mac-aarch64/deps/vlc") + into(destinationDir) + includeEmptyDirs = false + duplicatesStrategy = DuplicatesStrategy.INCLUDE + copyIfNeeded(destinationDir, copyDetails) + } + } + afterEvaluate { + doLast { + copyDetails.forEach { (destinationDir, details) -> + details.forEach { detail -> + val target = File(projectDir.absolutePath + File.separator + destinationDir + File.separator + detail.path) + if (target.exists()) { + target.setLastModified(detail.lastModified) + } + } + } + } } } } + +fun CopySpec.copyIfNeeded(destinationDir: String, into: MutableMap<String, ArrayList<FileCopyDetails>>) { + val details = arrayListOf<FileCopyDetails>() + eachFile { + val targetFile = File(destinationDir, path) + if (file.lastModified() == targetFile.lastModified() && file.length() == targetFile.length()) { + exclude() + } else { + details.add(this) + } + } + into[destinationDir] = details +} diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt new file mode 100644 index 0000000000..72c41a665b --- /dev/null +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -0,0 +1,57 @@ +package chat.simplex.desktop + +import chat.simplex.common.platform.* +import chat.simplex.common.showApp +import java.io.File +import java.nio.file.* +import java.nio.file.attribute.BasicFileAttributes +import java.nio.file.attribute.FileTime +import kotlin.io.path.setLastModifiedTime + +fun main() { + initHaskell() + initApp() + tmpDir.deleteRecursively() + tmpDir.mkdir() + return showApp() +} + +@Suppress("UnsafeDynamicallyLoadedCode") +private fun initHaskell() { + val libApp = "libapp-lib.${desktopPlatform.libExtension}" + val libsTmpDir = File(tmpDir.absolutePath + File.separator + "libs") + copyResources(desktopPlatform.libPath, libsTmpDir.toPath()) + System.load(File(libsTmpDir, libApp).absolutePath) + + vlcDir.deleteRecursively() + Files.move(File(libsTmpDir, "vlc").toPath(), vlcDir.toPath(), StandardCopyOption.REPLACE_EXISTING) + // No picture without preloading it, only sound. However, with libs from AppImage it works without preloading + //val libXcb = "libvlc_xcb_events.so.0.0.0" + //System.load(File(File(vlcDir, "vlc"), libXcb).absolutePath) + System.setProperty("jna.library.path", vlcDir.absolutePath) + //discoverVlcLibs(File(File(vlcDir, "vlc"), "plugins").absolutePath) + + libsTmpDir.deleteRecursively() + initHS() +} + +private fun copyResources(from: String, to: Path) { + val resource = Class.forName("chat.simplex.desktop.MainKt").getResource("")!!.toURI() + val fileSystem = FileSystems.newFileSystem(resource, emptyMap<String, String>()) + val resPath = fileSystem.getPath(from) + Files.walkFileTree(resPath, object: SimpleFileVisitor<Path>() { + override fun preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult { + Files.createDirectories(to.resolve(resPath.relativize(dir).toString())) + return FileVisitResult.CONTINUE + } + override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { + val dest = to.resolve(resPath.relativize(file).toString()) + Files.copy(file, dest, StandardCopyOption.REPLACE_EXISTING) + // Setting the same time on file as the time set in script that generates VLC libs + if (dest.toString().contains("." + desktopPlatform.libExtension)) { + dest.setLastModifiedTime(FileTime.fromMillis(0)) + } + return FileVisitResult.CONTINUE + } + }) +} diff --git a/apps/multiplatform/common/src/commonMain/resources/distribute/SimpleX.desktop b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/SimpleX.desktop similarity index 51% rename from apps/multiplatform/common/src/commonMain/resources/distribute/SimpleX.desktop rename to apps/multiplatform/desktop/src/jvmMain/resources/distribute/SimpleX.desktop index bcfe0c0801..79281a4dc8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/distribute/SimpleX.desktop +++ b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/SimpleX.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Type=Application Name=SimpleX Chat -Comment=Private and secure open-source messenger - no user IDs (not even random numbers). -Exec=simplex -Icon=simplex.png -Categories=Messenger; +Comment=Private and secure open-source messenger - no user IDs (not even random numbers) +Exec=/opt/simplex/bin/simplex +Icon=/opt/simplex/lib/simplex +Categories=Network;Chat; diff --git a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml new file mode 100644 index 0000000000..9f9b2d6d20 --- /dev/null +++ b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/chat.simplex.app.appdata.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="UTF-8"?> +<component type="desktop-application"> + <id>chat.simplex.app</id> + <launchable type="desktop-id">chat.simplex.app.desktop</launchable> + <metadata_license>FSFAP</metadata_license> + <project_license>AGPL-3.0</project_license> + <name>SimpleX</name> + <summary>Private and secure open-source messenger - no user IDs (not even random numbers)</summary> + + <description> + <p>Security assessment was done by Trail of Bits in November 2022.</p> + <p>SimpleX Chat features:</p> + <ul> + <li>end-to-end encrypted messages, with editing, replies and deletion of messages.</li> + <li>sending end-to-end encrypted images and files.</li> + <li>single-use and long-term user addresses.</li> + <li>secret chat groups - only group members know it exists and who is the member.</li> + <li>end-to-end encrypted audio and video calls.</li> + <li>private instant notifications.</li> + <li>portable chat profile - you can transfer your chat contacts and history to another device (terminal or mobile).</li> + <li>encrypted app database and files in the app storage (except videos).</li> + <li>18 interface languages.</li> + </ul> + <p>SimpleX Chat advantages:</p> + <ul> + <li><em>Full privacy of your identity, profile, contacts and metadata</em>: unlike any other existing messaging platform, SimpleX uses no phone numbers or any other identifiers assigned to the users - not even random numbers. This protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers.</li> + <li><em>Complete protection against spam and abuse</em>: as you have no identifier on SimpleX platform, you cannot be contacted unless you share a one-time invitation link or an optional temporary user address.</li> + <li><em>Full ownership, control and security of your data</em>: SimpleX stores all user data on client devices, the messages are only held temporarily on SimpleX relay servers until they are received.</li> + <li><em>Decentralized network</em>: you can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers.</li> + </ul> + <p>You can connect to anybody you know via link or scan QR code (in the video call or in person) and start sending messages instantly - no emails, phone numbers or passwords needed.</p> + <p>Your profile and contacts are only stored in the app on your device - our servers do not have access to this information.</p> + <p>All messages are end-to-end encrypted using open-source double-ratchet protocol; the messages are routed via our servers using open-source SimpleX Messaging Protocol.</p> + <p>Please send us any questions via the app or submit an issue on GitHub.</p> + <p>Follow us on Mastodon, Twitter and Reddit for the latest updates.</p> + <p>Once you install SimpleX Chat, "connect to developers" for any questions, to share feedback, and to discover the groups (the link for directory service will be in response).</p> + </description> + + <screenshots> + <screenshot type="default"> + <image>https://simplex.chat/img/simplex-desktop-linux-light.png</image> + </screenshot> + <screenshot> + <image>https://simplex.chat/img/simplex-desktop-linux-dark-1.png</image> + </screenshot> + <screenshot> + <image>https://simplex.chat/img/simplex-desktop-linux-dark-2.png</image> + </screenshot> + </screenshots> + + <url type="homepage">https://simplex.chat</url> + <url type="bugtracker">https://github.com/simplex-chat/simplex-chat/issues</url> + <url type="translate">https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat</url> + <url type="contact">https://simplex.chat/connect-team</url> + <url type="donation">https://github.com/simplex-chat/simplex-chat#help-us-with-donations</url> + + <content_rating type="oars-1.1"/> + + <releases> + <release version="5.3" date="2023-09-19"> + <description> + <p>- the first release of the desktop app!</p> + <p>- encrypted local files</p> + <p>- message delivery receipts in small groups</p> + <p>- 6 new interface languages: Arabic, Bulgarian, Finnish, Hebrew, Thai and Ukrainian</p> + </description> + </release> + </releases> +</component> diff --git a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.icns b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.icns new file mode 100644 index 0000000000..c0c91a98af Binary files /dev/null and b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.icns differ diff --git a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.ico b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.ico new file mode 100644 index 0000000000..783809be7b Binary files /dev/null and b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.ico differ diff --git a/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.png b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.png new file mode 100644 index 0000000000..d7454d4fb7 Binary files /dev/null and b/apps/multiplatform/desktop/src/jvmMain/resources/distribute/simplex.png differ diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 70cdf8d3af..0d047a7917 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -25,11 +25,12 @@ android.nonTransitiveRClass=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 -android.version_name=5.2.3 -android.version_code=144 +android.version_name=5.3.1 +android.version_code=154 -desktop.version_name=1.0 +desktop.version_name=5.3.1 +desktop.version_code=11 kotlin.version=1.8.20 gradle.plugin.version=7.4.2 -compose.version=1.4.0 +compose.version=1.4.3 diff --git a/apps/multiplatform/local.properties.example b/apps/multiplatform/local.properties.example new file mode 100644 index 0000000000..8fa9a47963 --- /dev/null +++ b/apps/multiplatform/local.properties.example @@ -0,0 +1,10 @@ +compression.level=0 +enable_debuggable=true +application_id.suffix=.debug +app.name=SimpleX Debug + +#desktop.mac.signing.identity=SimpleX Chat Ltd +#desktop.mac.signing.keychain=/path/to/simplex.keychain +#desktop.mac.notarization.apple_id=example@example.com +#desktop.mac.notarization.password=12345678 +#desktop.mac.notarization.team_id=XXXXXXXXXX diff --git a/apps/simplex-broadcast-bot/Main.hs b/apps/simplex-broadcast-bot/Main.hs index 966971633f..15bb743b56 100644 --- a/apps/simplex-broadcast-bot/Main.hs +++ b/apps/simplex-broadcast-bot/Main.hs @@ -1,76 +1,11 @@ -{-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE GADTs #-} -{-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} -{-# LANGUAGE OverloadedStrings #-} - module Main where -import Control.Concurrent (forkIO) -import Control.Concurrent.Async -import Control.Concurrent.STM -import Control.Monad.Reader -import qualified Data.Text as T -import Options -import Simplex.Chat.Bot -import Simplex.Chat.Controller +import Broadcast.Bot +import Broadcast.Options import Simplex.Chat.Core -import Simplex.Chat.Messages -import Simplex.Chat.Messages.CIContent -import Simplex.Chat.Options -import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Terminal (terminalChatConfig) -import Simplex.Chat.Types -import System.Directory (getAppUserDataDirectory) main :: IO () main = do opts <- welcomeGetOpts simplexChatCore terminalChatConfig (mkChatOpts opts) Nothing $ broadcastBot opts - -welcomeGetOpts :: IO BroadcastBotOpts -welcomeGetOpts = do - appDir <- getAppUserDataDirectory "simplex" - opts@BroadcastBotOpts {coreOptions = CoreChatOpts {dbFilePrefix}} <- getBroadcastBotOpts appDir "simplex_status_bot" - putStrLn $ "SimpleX Chat Bot v" ++ versionNumber - putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" - pure opts - -broadcastBot :: BroadcastBotOpts -> User -> ChatController -> IO () -broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _user cc = do - initializeBotAddress cc - race_ (forever $ void getLine) . forever $ do - (_, resp) <- atomically . readTBQueue $ outputQ cc - case resp of - CRContactConnected _ ct _ -> do - contactConnected ct - sendMessage cc ct welcomeMessage - CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) - | publisher `elem` publishers -> - if allowContent mc - then do - sendChatCmd cc "/contacts" >>= \case - CRContactsList _ cts -> void . forkIO $ do - let cts' = filter broadcastTo cts - forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc - sendReply $ "Forwarded to " <> show (length cts') <> " contact(s)" - r -> putStrLn $ "Error getting contacts list: " <> show r - else sendReply "!1 Message is not supported!" - | otherwise -> do - sendReply prohibitedMessage - deleteMessage cc ct $ chatItemId' ci - where - sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . textMsgContent - publisher = Publisher {contactId = contactId' ct, localDisplayName = localDisplayName' ct} - allowContent = \case - MCText _ -> True - MCLink {} -> True - MCImage {} -> True - _ -> False - broadcastTo ct'@Contact {activeConn = conn@Connection {connStatus}} = - (connStatus == ConnSndReady || connStatus == ConnReady) - && not (connDisabled conn) - && contactId' ct' /= contactId' ct - _ -> pure () - where - contactConnected ct = putStrLn $ T.unpack (localDisplayName' ct) <> " connected" diff --git a/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs new file mode 100644 index 0000000000..3a1be2ae08 --- /dev/null +++ b/apps/simplex-broadcast-bot/src/Broadcast/Bot.hs @@ -0,0 +1,71 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Broadcast.Bot where + +import Control.Concurrent (forkIO) +import Control.Concurrent.Async +import Control.Concurrent.STM +import Control.Monad.Reader +import qualified Data.Text as T +import Broadcast.Options +import Simplex.Chat.Bot +import Simplex.Chat.Bot.KnownContacts +import Simplex.Chat.Controller +import Simplex.Chat.Core +import Simplex.Chat.Messages +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Options +import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Types +import System.Directory (getAppUserDataDirectory) + +welcomeGetOpts :: IO BroadcastBotOpts +welcomeGetOpts = do + appDir <- getAppUserDataDirectory "simplex" + opts@BroadcastBotOpts {coreOptions = CoreChatOpts {dbFilePrefix}} <- getBroadcastBotOpts appDir "simplex_status_bot" + putStrLn $ "SimpleX Chat Bot v" ++ versionNumber + putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + pure opts + +broadcastBot :: BroadcastBotOpts -> User -> ChatController -> IO () +broadcastBot BroadcastBotOpts {publishers, welcomeMessage, prohibitedMessage} _user cc = do + initializeBotAddress cc + race_ (forever $ void getLine) . forever $ do + (_, resp) <- atomically . readTBQueue $ outputQ cc + case resp of + CRContactConnected _ ct _ -> do + contactConnected ct + sendMessage cc ct welcomeMessage + CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc}) + | publisher `elem` publishers -> + if allowContent mc + then do + sendChatCmd cc ListContacts >>= \case + CRContactsList _ cts -> void . forkIO $ do + let cts' = filter broadcastTo cts + forM_ cts' $ \ct' -> sendComposedMessage cc ct' Nothing mc + sendReply $ "Forwarded to " <> show (length cts') <> " contact(s)" + r -> putStrLn $ "Error getting contacts list: " <> show r + else sendReply "!1 Message is not supported!" + | otherwise -> do + sendReply prohibitedMessage + deleteMessage cc ct $ chatItemId' ci + where + sendReply = sendComposedMessage cc ct (Just $ chatItemId' ci) . textMsgContent + publisher = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} + allowContent = \case + MCText _ -> True + MCLink {} -> True + MCImage {} -> True + _ -> False + broadcastTo ct'@Contact {activeConn = conn@Connection {connStatus}} = + (connStatus == ConnSndReady || connStatus == ConnReady) + && not (connDisabled conn) + && contactId' ct' /= contactId' ct + _ -> pure () + where + contactConnected ct = putStrLn $ T.unpack (localDisplayName' ct) <> " connected" diff --git a/apps/simplex-broadcast-bot/Options.hs b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs similarity index 69% rename from apps/simplex-broadcast-bot/Options.hs rename to apps/simplex-broadcast-bot/src/Broadcast/Options.hs index 994884760d..76b349a499 100644 --- a/apps/simplex-broadcast-bot/Options.hs +++ b/apps/simplex-broadcast-bot/src/Broadcast/Options.hs @@ -4,48 +4,33 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -module Options where +module Broadcast.Options where -import qualified Data.Attoparsec.ByteString.Char8 as A -import Data.Int (Int64) import Data.Maybe (fromMaybe) -import Data.Text (Text) -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import Options.Applicative +import Simplex.Chat.Bot.KnownContacts import Simplex.Chat.Controller (updateStr, versionNumber, versionString) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts, coreChatOptsP) -import Simplex.Messaging.Parsers (parseAll) -import Simplex.Messaging.Util (safeDecodeUtf8) - -data Publisher = Publisher - { contactId :: Int64, - localDisplayName :: Text - } - deriving (Eq) data BroadcastBotOpts = BroadcastBotOpts { coreOptions :: CoreChatOpts, - publishers :: [Publisher], + publishers :: [KnownContact], welcomeMessage :: String, prohibitedMessage :: String } -defaultWelcomeMessage :: [Publisher] -> String -defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> publisherNames ps <> "." +defaultWelcomeMessage :: [KnownContact] -> String +defaultWelcomeMessage ps = "Hello! I am a broadcast bot.\nI broadcast messages to all connected users from " <> knownContactNames ps <> "." -defaultProhibitedMessage :: [Publisher] -> String -defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> publisherNames ps <> ". Your message is deleted." - -publisherNames :: [Publisher] -> String -publisherNames = T.unpack . T.intercalate ", " . map (("@" <>) . localDisplayName) +defaultProhibitedMessage :: [KnownContact] -> String +defaultProhibitedMessage ps = "Sorry, only these users can broadcast messages: " <> knownContactNames ps <> ". Your message is deleted." broadcastBotOpts :: FilePath -> FilePath -> Parser BroadcastBotOpts broadcastBotOpts appDir defaultDbFileName = do coreOptions <- coreChatOptsP appDir defaultDbFileName publishers <- option - parsePublishers + parseKnownContacts ( long "publishers" <> metavar "PUBLISHERS" <> help "Comma-separated list of publishers in the format CONTACT_ID:DISPLAY_NAME whose messages will be broadcasted" @@ -74,17 +59,6 @@ broadcastBotOpts appDir defaultDbFileName = do prohibitedMessage = fromMaybe (defaultProhibitedMessage publishers) prohibitedMessage_ } -parsePublishers :: ReadM [Publisher] -parsePublishers = eitherReader $ parseAll publishersP . encodeUtf8 . T.pack - -publishersP :: A.Parser [Publisher] -publishersP = publisherP `A.sepBy1` A.char ',' - where - publisherP = do - contactId <- A.decimal <* A.char ':' - localDisplayName <- safeDecodeUtf8 <$> A.takeTill (A.inClass ", ") - pure Publisher {contactId, localDisplayName} - getBroadcastBotOpts :: FilePath -> FilePath -> IO BroadcastBotOpts getBroadcastBotOpts appDir defaultDbFileName = execParser $ diff --git a/apps/simplex-chat/Main.hs b/apps/simplex-chat/Main.hs index b16bbd8ee2..8dd02623e2 100644 --- a/apps/simplex-chat/Main.hs +++ b/apps/simplex-chat/Main.hs @@ -28,7 +28,7 @@ main = do t <- withTerminal pure simplexChatTerminal terminalChatConfig opts t else simplexChatCore terminalChatConfig opts Nothing $ \user cc -> do - r <- sendChatCmd cc chatCmd + r <- sendChatCmdStr cc chatCmd ts <- getCurrentTime tz <- getCurrentTimeZone putStrLn $ serializeChatResponse (Just user) ts tz r diff --git a/apps/simplex-directory-service/Main.hs b/apps/simplex-directory-service/Main.hs new file mode 100644 index 0000000000..434e42d851 --- /dev/null +++ b/apps/simplex-directory-service/Main.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE NamedFieldPuns #-} + +module Main where + +import Directory.Options +import Directory.Service +import Directory.Store +import Simplex.Chat.Core +import Simplex.Chat.Terminal (terminalChatConfig) + +main :: IO () +main = do + opts@DirectoryOpts {directoryLog} <- welcomeGetOpts + st <- restoreDirectoryStore directoryLog + simplexChatCore terminalChatConfig (mkChatOpts opts) Nothing $ directoryService st opts diff --git a/apps/simplex-directory-service/README.md b/apps/simplex-directory-service/README.md new file mode 100644 index 0000000000..b64e018adb --- /dev/null +++ b/apps/simplex-directory-service/README.md @@ -0,0 +1,5 @@ +# SimpleX Directory Service + +The service is currently a chat bot that allows to register and search for groups. + +Superusers are configured via CLI options. diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs new file mode 100644 index 0000000000..dab9ceb77e --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -0,0 +1,161 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE StandaloneDeriving #-} + +module Directory.Events + ( DirectoryEvent (..), + DirectoryCmd (..), + ADirectoryCmd (..), + DirectoryRole (..), + SDirectoryRole (..), + crDirectoryEvent, + ) +where + +import Control.Applicative ((<|>)) +import Data.Attoparsec.Text (Parser) +import qualified Data.Attoparsec.Text as A +import Data.Text (Text) +import qualified Data.Text as T +import Directory.Store +import Simplex.Chat.Controller +import Simplex.Chat.Messages +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Types +import Data.Char (isSpace) +import Data.Either (fromRight) + +data DirectoryEvent + = DEContactConnected Contact + | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} + | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} + | DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo} + | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed + | DEServiceRoleChanged GroupInfo GroupMemberRole + | DEContactRemovedFromGroup ContactId GroupInfo + | DEContactLeftGroup ContactId GroupInfo + | DEServiceRemovedFromGroup GroupInfo + | DEGroupDeleted GroupInfo + | DEUnsupportedMessage Contact ChatItemId + | DEItemEditIgnored Contact + | DEItemDeleteIgnored Contact + | DEContactCommand Contact ChatItemId ADirectoryCmd + deriving (Show) + +crDirectoryEvent :: ChatResponse -> Maybe DirectoryEvent +crDirectoryEvent = \case + CRContactConnected {contact} -> Just $ DEContactConnected contact + CRReceivedGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} -> Just $ DEGroupInvitation {contact, groupInfo, fromMemberRole, memberRole} + CRUserJoinedGroup {groupInfo, hostMember} -> (\contactId -> DEServiceJoinedGroup {contactId, groupInfo, hostMember}) <$> memberContactId hostMember + CRGroupUpdated {fromGroup, toGroup, member_} -> (\contactId -> DEGroupUpdated {contactId, fromGroup, toGroup}) <$> (memberContactId =<< member_) + CRMemberRole {groupInfo, member, toRole} + | groupMemberId' member == groupMemberId' (membership groupInfo) -> Just $ DEServiceRoleChanged groupInfo toRole + | otherwise -> (\ctId -> DEContactRoleChanged groupInfo ctId toRole) <$> memberContactId member + CRDeletedMember {groupInfo, deletedMember} -> (`DEContactRemovedFromGroup` groupInfo) <$> memberContactId deletedMember + CRLeftMember {groupInfo, member} -> (`DEContactLeftGroup` groupInfo) <$> memberContactId member + CRDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo + CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo + CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct + CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct + CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} -> + Just $ case (mc, itemLive) of + (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly directoryCmdP $ T.dropWhileEnd isSpace t + _ -> DEUnsupportedMessage ct ciId + where + ciId = chatItemId' ci + err = ADC SDRUser DCUnknownCommand + _ -> Nothing + +data DirectoryRole = DRUser | DRSuperUser + +data SDirectoryRole (r :: DirectoryRole) where + SDRUser :: SDirectoryRole 'DRUser + SDRSuperUser :: SDirectoryRole 'DRSuperUser + +deriving instance Show (SDirectoryRole r) + +data DirectoryCmdTag (r :: DirectoryRole) where + DCHelp_ :: DirectoryCmdTag 'DRUser + DCConfirmDuplicateGroup_ :: DirectoryCmdTag 'DRUser + DCListUserGroups_ :: DirectoryCmdTag 'DRUser + DCDeleteGroup_ :: DirectoryCmdTag 'DRUser + DCApproveGroup_ :: DirectoryCmdTag 'DRSuperUser + DCRejectGroup_ :: DirectoryCmdTag 'DRSuperUser + DCSuspendGroup_ :: DirectoryCmdTag 'DRSuperUser + DCResumeGroup_ :: DirectoryCmdTag 'DRSuperUser + DCListLastGroups_ :: DirectoryCmdTag 'DRSuperUser + DCExecuteCommand_ :: DirectoryCmdTag 'DRSuperUser + +deriving instance Show (DirectoryCmdTag r) + +data ADirectoryCmdTag = forall r. ADCT (SDirectoryRole r) (DirectoryCmdTag r) + +data DirectoryCmd (r :: DirectoryRole) where + DCHelp :: DirectoryCmd 'DRUser + DCSearchGroup :: Text -> DirectoryCmd 'DRUser + DCConfirmDuplicateGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser + DCListUserGroups :: DirectoryCmd 'DRUser + DCDeleteGroup :: UserGroupRegId -> GroupName -> DirectoryCmd 'DRUser + DCApproveGroup :: {groupId :: GroupId, displayName :: GroupName, groupApprovalId :: GroupApprovalId} -> DirectoryCmd 'DRSuperUser + DCRejectGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser + DCSuspendGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser + DCResumeGroup :: GroupId -> GroupName -> DirectoryCmd 'DRSuperUser + DCListLastGroups :: Int -> DirectoryCmd 'DRSuperUser + DCExecuteCommand :: String -> DirectoryCmd 'DRSuperUser + DCUnknownCommand :: DirectoryCmd 'DRUser + DCCommandError :: DirectoryCmdTag r -> DirectoryCmd r + +deriving instance Show (DirectoryCmd r) + +data ADirectoryCmd = forall r. ADC (SDirectoryRole r) (DirectoryCmd r) + +deriving instance Show ADirectoryCmd + +directoryCmdP :: Parser ADirectoryCmd +directoryCmdP = + (A.char '/' *> cmdStrP) <|> (ADC SDRUser . DCSearchGroup <$> A.takeText) + where + cmdStrP = + (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) + <|> pure (ADC SDRUser DCUnknownCommand) + tagP = A.takeTill (== ' ') >>= \case + "help" -> u DCHelp_ + "h" -> u DCHelp_ + "confirm" -> u DCConfirmDuplicateGroup_ + "list" -> u DCListUserGroups_ + "ls" -> u DCListUserGroups_ + "delete" -> u DCDeleteGroup_ + "approve" -> su DCApproveGroup_ + "reject" -> su DCRejectGroup_ + "suspend" -> su DCSuspendGroup_ + "resume" -> su DCResumeGroup_ + "last" -> su DCListLastGroups_ + "exec" -> su DCExecuteCommand_ + "x" -> su DCExecuteCommand_ + _ -> fail "bad command tag" + where + u = pure . ADCT SDRUser + su = pure . ADCT SDRSuperUser + cmdP :: DirectoryCmdTag r -> Parser (DirectoryCmd r) + cmdP = \case + DCHelp_ -> pure DCHelp + DCConfirmDuplicateGroup_ -> gc DCConfirmDuplicateGroup + DCListUserGroups_ -> pure DCListUserGroups + DCDeleteGroup_ -> gc DCDeleteGroup + DCApproveGroup_ -> do + (groupId, displayName) <- gc (,) + groupApprovalId <- A.space *> A.decimal + pure $ DCApproveGroup {groupId, displayName, groupApprovalId} + DCRejectGroup_ -> gc DCRejectGroup + DCSuspendGroup_ -> gc DCSuspendGroup + DCResumeGroup_ -> gc DCResumeGroup + DCListLastGroups_ -> DCListLastGroups <$> (A.space *> A.decimal <|> pure 10) + DCExecuteCommand_ -> DCExecuteCommand . T.unpack <$> (A.space *> A.takeText) + where + gc f = f <$> (A.space *> A.decimal <* A.char ':') <*> A.takeTill (== ' ') diff --git a/apps/simplex-directory-service/src/Directory/Options.hs b/apps/simplex-directory-service/src/Directory/Options.hs new file mode 100644 index 0000000000..1f06afe116 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Options.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE ApplicativeDo #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Directory.Options + ( DirectoryOpts (..), + getDirectoryOpts, + mkChatOpts, + ) +where + +import Options.Applicative +import Simplex.Chat.Bot.KnownContacts +import Simplex.Chat.Controller (updateStr, versionNumber, versionString) +import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts, coreChatOptsP) + +data DirectoryOpts = DirectoryOpts + { coreOptions :: CoreChatOpts, + superUsers :: [KnownContact], + directoryLog :: Maybe FilePath, + serviceName :: String, + testing :: Bool + } + +directoryOpts :: FilePath -> FilePath -> Parser DirectoryOpts +directoryOpts appDir defaultDbFileName = do + coreOptions <- coreChatOptsP appDir defaultDbFileName + superUsers <- + option + parseKnownContacts + ( long "super-users" + <> metavar "SUPER_USERS" + <> help "Comma-separated list of super-users in the format CONTACT_ID:DISPLAY_NAME who will be allowed to manage the directory" + ) + directoryLog <- + Just <$> + strOption + ( long "directory-file" + <> metavar "DIRECTORY_FILE" + <> help "Append only log for directory state" + ) + serviceName <- + strOption + ( long "service-name" + <> metavar "SERVICE_NAME" + <> help "The display name of the directory service bot, without *'s and spaces (SimpleX-Directory)" + <> value "SimpleX-Directory" + ) + pure + DirectoryOpts + { coreOptions, + superUsers, + directoryLog, + serviceName, + testing = False + } + +getDirectoryOpts :: FilePath -> FilePath -> IO DirectoryOpts +getDirectoryOpts appDir defaultDbFileName = + execParser $ + info + (helper <*> versionOption <*> directoryOpts appDir defaultDbFileName) + (header versionStr <> fullDesc <> progDesc "Start SimpleX Directory Service with DB_FILE, DIRECTORY_FILE and SUPER_USERS options") + where + versionStr = versionString versionNumber + versionOption = infoOption versionAndUpdate (long "version" <> short 'v' <> help "Show version") + versionAndUpdate = versionStr <> "\n" <> updateStr + +mkChatOpts :: DirectoryOpts -> ChatOpts +mkChatOpts DirectoryOpts {coreOptions} = + ChatOpts + { coreOptions, + chatCmd = "", + chatCmdDelay = 3, + chatServerPort = Nothing, + optFilesFolder = Nothing, + showReactions = False, + allowInstantFiles = True, + autoAcceptFileSize = 0, + muteNotifications = True, + maintenance = False + } diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs new file mode 100644 index 0000000000..09ab424cf0 --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -0,0 +1,551 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE MultiWayIf #-} + +module Directory.Service + ( welcomeGetOpts, + directoryService, + ) +where + +import Control.Concurrent (forkIO) +import Control.Concurrent.Async +import Control.Concurrent.STM +import Control.Monad.Reader +import qualified Data.ByteString.Char8 as B +import Data.List (sortOn) +import Data.Maybe (fromMaybe, maybeToList) +import Data.Ord (Down(..)) +import qualified Data.Set as S +import Data.Text (Text) +import qualified Data.Text as T +import Data.Time.Clock (getCurrentTime) +import Data.Time.LocalTime (getCurrentTimeZone) +import Directory.Events +import Directory.Options +import Directory.Store +import Simplex.Chat.Bot +import Simplex.Chat.Bot.KnownContacts +import Simplex.Chat.Controller +import Simplex.Chat.Core +import Simplex.Chat.Messages +import Simplex.Chat.Options +import Simplex.Chat.Protocol (MsgContent (..)) +import Simplex.Chat.Types +import Simplex.Chat.View (serializeChatResponse) +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util (safeDecodeUtf8, tshow, ($>>=), (<$$>)) +import System.Directory (getAppUserDataDirectory) + +data GroupProfileUpdate = GPNoServiceLink | GPServiceLinkAdded | GPServiceLinkRemoved | GPHasServiceLink | GPServiceLinkError + +data DuplicateGroup + = DGUnique -- display name or full name is unique + | DGRegistered -- the group with the same names is registered, additional confirmation is required + | DGReserved -- the group with the same names is listed, the registration is not allowed + +data GroupRolesStatus + = GRSOk + | GRSServiceNotAdmin + | GRSContactNotOwner + | GRSBadRoles + deriving (Eq) + +welcomeGetOpts :: IO DirectoryOpts +welcomeGetOpts = do + appDir <- getAppUserDataDirectory "simplex" + opts@DirectoryOpts {coreOptions = CoreChatOpts {dbFilePrefix}, testing} <- getDirectoryOpts appDir "simplex_directory_service" + unless testing $ do + putStrLn $ "SimpleX Directory Service Bot v" ++ versionNumber + putStrLn $ "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db" + pure opts + +directoryService :: DirectoryStore -> DirectoryOpts -> User -> ChatController -> IO () +directoryService st DirectoryOpts {superUsers, serviceName, testing} user@User {userId} cc = do + initializeBotAddress' (not testing) cc + race_ (forever $ void getLine) . forever $ do + (_, resp) <- atomically . readTBQueue $ outputQ cc + forM_ (crDirectoryEvent resp) $ \case + DEContactConnected ct -> deContactConnected ct + DEGroupInvitation {contact = ct, groupInfo = g, fromMemberRole, memberRole} -> deGroupInvitation ct g fromMemberRole memberRole + DEServiceJoinedGroup ctId g owner -> deServiceJoinedGroup ctId g owner + DEGroupUpdated {contactId, fromGroup, toGroup} -> deGroupUpdated contactId fromGroup toGroup + DEContactRoleChanged g ctId role -> deContactRoleChanged g ctId role + DEServiceRoleChanged g role -> deServiceRoleChanged g role + DEContactRemovedFromGroup ctId g -> deContactRemovedFromGroup ctId g + DEContactLeftGroup ctId g -> deContactLeftGroup ctId g + DEServiceRemovedFromGroup g -> deServiceRemovedFromGroup g + DEGroupDeleted _g -> pure () + DEUnsupportedMessage _ct _ciId -> pure () + DEItemEditIgnored _ct -> pure () + DEItemDeleteIgnored _ct -> pure () + DEContactCommand ct ciId aCmd -> case aCmd of + ADC SDRUser cmd -> deUserCommand ct ciId cmd + ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd + where + withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId + notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s + notifyOwner GroupReg {dbContactId} = sendMessage' cc dbContactId + ctId `isOwner` GroupReg {dbContactId} = ctId == dbContactId + withGroupReg GroupInfo {groupId, localDisplayName} err action = do + atomically (getGroupReg st groupId) >>= \case + Just gr -> action gr + Nothing -> putStrLn $ T.unpack $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId + groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} = + n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d + userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName + userGroupReference' GroupReg {userGroupRegId} displayName = groupReference' userGroupRegId displayName + groupReference GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = groupReference' groupId displayName + groupReference' groupId displayName = "ID " <> show groupId <> " (" <> T.unpack displayName <> ")" + groupAlreadyListed GroupInfo {groupProfile = GroupProfile {displayName, fullName}} = + T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already listed in the directory, please choose another name." + + getGroups :: Text -> IO (Maybe [(GroupInfo, GroupSummary)]) + getGroups search = + sendChatCmd cc (APIListGroups userId Nothing $ Just $ T.unpack search) >>= \case + CRGroupsList {groups} -> pure $ Just groups + _ -> pure Nothing + + getDuplicateGroup :: GroupInfo -> IO (Maybe DuplicateGroup) + getDuplicateGroup GroupInfo {groupId, groupProfile = GroupProfile {displayName, fullName}} = + getGroups fullName >>= mapM duplicateGroup + where + sameGroup (GroupInfo {groupId = gId, groupProfile = GroupProfile {displayName = n, fullName = fn}}, _) = + gId /= groupId && n == displayName && fn == fullName + duplicateGroup [] = pure DGUnique + duplicateGroup groups = do + let gs = filter sameGroup groups + if null gs + then pure DGUnique + else do + (lgs, rgs) <- atomically $ (,) <$> readTVar (listedGroups st) <*> readTVar (reservedGroups st) + let reserved = any (\(GroupInfo {groupId = gId}, _) -> gId `S.member` lgs || gId `S.member` rgs) gs + pure $ if reserved then DGReserved else DGRegistered + + processInvitation :: Contact -> GroupInfo -> IO () + processInvitation ct g@GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = do + void $ addGroupReg st ct g GRSProposed + r <- sendChatCmd cc $ APIJoinGroup groupId + sendMessage cc ct $ T.unpack $ case r of + CRUserAcceptedGroupSent {} -> "Joining the group " <> displayName <> "…" + _ -> "Error joining group " <> displayName <> ", please re-send the invitation!" + + deContactConnected :: Contact -> IO () + deContactConnected ct = when (contactDirect ct) $ do + unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected" + sendMessage cc ct $ + "Welcome to " <> serviceName <> " service!\n\ + \Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ + \For example, send _privacy_ to find groups about privacy.\n\n\ + \Content and privacy policy: https://simplex.chat/docs/directory.html" + + deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () + deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do + case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of + Just msg -> sendMessage cc ct msg + Nothing -> getDuplicateGroup g >>= \case + Just DGUnique -> processInvitation ct g + Just DGRegistered -> askConfirmation + Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g + Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + where + askConfirmation = do + ugrId <- addGroupReg st ct g GRSPendingConfirmation + sendMessage cc ct $ T.unpack $ "The group " <> displayName <> " (" <> fullName <> ") is already submitted to the directory.\nTo confirm the registration, please send:" + sendMessage cc ct $ "/confirm " <> show ugrId <> ":" <> T.unpack displayName + + badRolesMsg :: GroupRolesStatus -> Maybe String + badRolesMsg = \case + GRSOk -> Nothing + GRSServiceNotAdmin -> Just "You must have a group *owner* role to register the group" + GRSContactNotOwner -> Just "You must grant directory service *admin* role to register the group" + GRSBadRoles -> Just "You must have a group *owner* role and you must grant directory service *admin* role to register the group" + + getGroupRolesStatus :: GroupInfo -> GroupReg -> IO (Maybe GroupRolesStatus) + getGroupRolesStatus GroupInfo {membership = GroupMember {memberRole = serviceRole}} gr = + rStatus <$$> getGroupMember gr + where + rStatus GroupMember {memberRole} = groupRolesStatus memberRole serviceRole + + groupRolesStatus :: GroupMemberRole -> GroupMemberRole -> GroupRolesStatus + groupRolesStatus contactRole serviceRole = case (contactRole, serviceRole) of + (GROwner, GRAdmin) -> GRSOk + (_, GRAdmin) -> GRSServiceNotAdmin + (GROwner, _) -> GRSContactNotOwner + _ -> GRSBadRoles + + getGroupMember :: GroupReg -> IO (Maybe GroupMember) + getGroupMember GroupReg {dbGroupId, dbOwnerMemberId} = + readTVarIO dbOwnerMemberId + $>>= \mId -> resp <$> sendChatCmd cc (APIGroupMemberInfo dbGroupId mId) + where + resp = \case + CRGroupMemberInfo {member} -> Just member + _ -> Nothing + + deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () + deServiceJoinedGroup ctId g owner = + withGroupReg g "joined group" $ \gr -> + when (ctId `isOwner` gr) $ do + setGroupRegOwner st gr owner + let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g + notifyOwner gr $ T.unpack $ "Joined the group " <> displayName <> ", creating the link…" + sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case + CRGroupLinkCreated {connReqContact} -> do + setGroupStatus st gr GRSPendingUpdate + notifyOwner gr + "Created the public link to join the group via this directory service that is always online.\n\n\ + \Please add it to the group welcome message.\n\ + \For example, add:" + notifyOwner gr $ "Link to join the group " <> T.unpack displayName <> ": " <> B.unpack (strEncode connReqContact) + CRChatCmdError _ (ChatError e) -> case e of + CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." + CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." + CEGroupNotJoined _ -> notifyOwner gr $ unexpectedError "group not joined" + CEGroupMemberNotActive -> notifyOwner gr $ unexpectedError "service membership is not active" + _ -> notifyOwner gr $ unexpectedError "can't create group link" + _ -> notifyOwner gr $ unexpectedError "can't create group link" + + deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO () + deGroupUpdated ctId fromGroup toGroup = + unless (sameProfile p p') $ do + withGroupReg toGroup "group updated" $ \gr -> do + let userGroupRef = userGroupReference gr toGroup + readTVarIO (groupRegStatus gr) >>= \case + GRSPendingConfirmation -> pure () + GRSProposed -> pure () + GRSPendingUpdate -> groupProfileUpdate >>= \case + GPNoServiceLink -> + when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." + GPServiceLinkAdded + | ctId `isOwner` gr -> groupLinkAdded gr + | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." + GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." + GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr + GPServiceLinkError -> do + when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." + putStrLn $ "Error: no group link for " <> userGroupRef + GRSPendingApproval n -> processProfileChange gr $ n + 1 + GRSActive -> processProfileChange gr 1 + GRSSuspended -> processProfileChange gr 1 + GRSSuspendedBadRoles -> processProfileChange gr 1 + GRSRemoved -> pure () + where + isInfix l d_ = l `T.isInfixOf` fromMaybe "" d_ + GroupInfo {groupId, groupProfile = p} = fromGroup + GroupInfo {groupProfile = p'} = toGroup + sameProfile + GroupProfile {displayName = n, fullName = fn, image = i, description = d} + GroupProfile {displayName = n', fullName = fn', image = i', description = d'} = + n == n' && fn == fn' && i == i' && d == d' + groupLinkAdded gr = do + getDuplicateGroup toGroup >>= \case + Nothing -> notifyOwner gr "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> notifyOwner gr $ groupAlreadyListed toGroup + _ -> do + let gaId = 1 + setGroupStatus st gr $ GRSPendingApproval gaId + notifyOwner gr $ "Thank you! The group link for " <> userGroupReference gr toGroup <> " is added to the welcome message.\nYou will be notified once the group is added to the directory - it may take up to 24 hours." + checkRolesSendToApprove gr gaId + processProfileChange gr n' = do + setGroupStatus st gr GRSPendingUpdate + let userGroupRef = userGroupReference gr toGroup + groupRef = groupReference toGroup + groupProfileUpdate >>= \case + GPNoServiceLink -> do + notifyOwner gr $ "The group profile is updated " <> userGroupRef <> ", but no link is added to the welcome message.\n\nThe group will remain hidden from the directory until the group link is added and the group is re-approved." + GPServiceLinkRemoved -> do + notifyOwner gr $ "The group link for " <> userGroupRef <> " is removed from the welcome message.\n\nThe group is hidden from the directory until the group link is added and the group is re-approved." + notifySuperUsers $ "The group link is removed from " <> groupRef <> ", de-listed." + GPServiceLinkAdded -> do + setGroupStatus st gr $ GRSPendingApproval n' + notifyOwner gr $ "The group link is added to " <> userGroupRef <> "!\nIt is hidden from the directory until approved." + notifySuperUsers $ "The group link is added to " <> groupRef <> "." + checkRolesSendToApprove gr n' + GPHasServiceLink -> do + setGroupStatus st gr $ GRSPendingApproval n' + notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." + notifySuperUsers $ "The group " <> groupRef <> " is updated." + checkRolesSendToApprove gr n' + GPServiceLinkError -> putStrLn $ "Error: no group link for " <> groupRef <> " pending approval." + groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) + where + profileUpdate = \case + CRGroupLink {connReqContact} -> + let groupLink = safeDecodeUtf8 $ strEncode connReqContact + hadLinkBefore = groupLink `isInfix` description p + hasLinkNow = groupLink `isInfix` description p' + in if + | hadLinkBefore && hasLinkNow -> GPHasServiceLink + | hadLinkBefore -> GPServiceLinkRemoved + | hasLinkNow -> GPServiceLinkAdded + | otherwise -> GPNoServiceLink + _ -> GPServiceLinkError + checkRolesSendToApprove gr gaId = do + (badRolesMsg <$$> getGroupRolesStatus toGroup gr) >>= \case + Nothing -> notifyOwner gr "Error: getGroupRolesStatus. Please notify the developers." + Just (Just msg) -> notifyOwner gr msg + Just Nothing -> sendToApprove toGroup gr gaId + + sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () + sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do + ct_ <- getContact cc dbContactId + gr_ <- getGroupAndSummary cc dbGroupId + let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ + text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ + <> "\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:" + msg = maybe (MCText text) (\image -> MCImage {text, image}) image' + withSuperUsers $ \cId -> do + sendComposedMessage' cc cId Nothing msg + sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> T.unpack displayName <> " " <> show gaId + + deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () + deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = + withGroupReg g "contact role changed" $ \gr -> do + let userGroupRef = userGroupReference gr g + uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole + when (ctId `isOwner` gr) $ do + readTVarIO (groupRegStatus gr) >>= \case + GRSSuspendedBadRoles -> when (rStatus == GRSOk) $ do + setGroupStatus st gr GRSActive + notifyOwner gr $ uCtRole <> ".\n\nThe group is listed in the directory again." + notifySuperUsers $ "The group " <> groupRef <> " is listed " <> suCtRole + GRSPendingApproval gaId -> when (rStatus == GRSOk) $ do + sendToApprove g gr gaId + notifyOwner gr $ uCtRole <> ".\n\nThe group is submitted for approval." + GRSActive -> when (rStatus /= GRSOk) $ do + setGroupStatus st gr GRSSuspendedBadRoles + notifyOwner gr $ uCtRole <> ".\n\nThe group is no longer listed in the directory." + notifySuperUsers $ "The group " <> groupRef <> " is de-listed " <> suCtRole + _ -> pure () + where + rStatus = groupRolesStatus contactRole serviceRole + groupRef = groupReference g + ctRole = "*" <> B.unpack (strEncode contactRole) <> "*" + suCtRole = "(user role is set to " <> ctRole <> ")." + + deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () + deServiceRoleChanged g serviceRole = do + withGroupReg g "service role changed" $ \gr -> do + let userGroupRef = userGroupReference gr g + uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole + readTVarIO (groupRegStatus gr) >>= \case + GRSSuspendedBadRoles -> when (serviceRole == GRAdmin) $ + whenContactIsOwner gr $ do + setGroupStatus st gr GRSActive + notifyOwner gr $ uSrvRole <> ".\n\nThe group is listed in the directory again." + notifySuperUsers $ "The group " <> groupRef <> " is listed " <> suSrvRole + GRSPendingApproval gaId -> when (serviceRole == GRAdmin) $ + whenContactIsOwner gr $ do + sendToApprove g gr gaId + notifyOwner gr $ uSrvRole <> ".\n\nThe group is submitted for approval." + GRSActive -> when (serviceRole /= GRAdmin) $ do + setGroupStatus st gr GRSSuspendedBadRoles + notifyOwner gr $ uSrvRole <> ".\n\nThe group is no longer listed in the directory." + notifySuperUsers $ "The group " <> groupRef <> " is de-listed " <> suSrvRole + _ -> pure () + where + groupRef = groupReference g + srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*" + suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." + whenContactIsOwner gr action = + getGroupMember gr >>= + mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) + + deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO () + deContactRemovedFromGroup ctId g = + withGroupReg g "contact removed" $ \gr -> do + when (ctId `isOwner` gr) $ do + setGroupStatus st gr GRSRemoved + notifyOwner gr $ "You are removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." + notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." + + deContactLeftGroup :: ContactId -> GroupInfo -> IO () + deContactLeftGroup ctId g = + withGroupReg g "contact left" $ \gr -> do + when (ctId `isOwner` gr) $ do + setGroupStatus st gr GRSRemoved + notifyOwner gr $ "You left the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." + notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." + + deServiceRemovedFromGroup :: GroupInfo -> IO () + deServiceRemovedFromGroup g = + withGroupReg g "service removed" $ \gr -> do + setGroupStatus st gr GRSRemoved + notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." + notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (directory service is removed)." + + deUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRUser -> IO () + deUserCommand ct ciId = \case + DCHelp -> + sendMessage cc ct $ + "You must be the owner to add the group to the directory:\n\ + \1. Invite " <> serviceName <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ + \2. " <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\ + \3. You will then need to add this link to the group welcome message.\n\ + \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ + \Start from inviting the bot to your group as admin - it will guide you through the process" + DCSearchGroup s -> + getGroups s >>= \case + Just groups -> + atomically (filterListedGroups st groups) >>= \case + [] -> sendReply "No groups found" + gs -> do + sendReply $ "Found " <> show (length gs) <> " group(s)" <> if length gs > 10 then ", sending 10." else "" + void . forkIO $ forM_ (take 10 $ sortOn (Down . currentMembers . snd) gs) $ + \(GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do + let membersStr = "_" <> tshow currentMembers <> " members_" + text = groupInfoText p <> "\n" <> membersStr + msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ + sendComposedMessage cc ct Nothing msg + Nothing -> sendReply "Error: getGroups. Please notify the developers." + DCConfirmDuplicateGroup ugrId gName -> + atomically (getUserGroupReg st (contactId' ct) ugrId) >>= \case + Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" + Just GroupReg {dbGroupId, groupRegStatus} -> do + getGroup cc dbGroupId >>= \case + Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" + Just g@GroupInfo {groupProfile = GroupProfile {displayName}} + | displayName == gName -> + readTVarIO groupRegStatus >>= \case + GRSPendingConfirmation -> do + getDuplicateGroup g >>= \case + Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g + _ -> processInvitation ct g + _ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation." + | otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName + DCListUserGroups -> + atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do + sendReply $ show (length grs) <> " registered group(s)" + void . forkIO $ forM_ (reverse grs) $ \gr@GroupReg {userGroupRegId} -> + sendGroupInfo ct gr userGroupRegId Nothing + DCDeleteGroup _ugrId _gName -> pure () + DCUnknownCommand -> sendReply "Unknown command" + DCCommandError tag -> sendReply $ "Command error: " <> show tag + where + sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + + deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () + deSuperUserCommand ct ciId cmd + | superUser `elem` superUsers = case cmd of + DCApproveGroup {groupId, displayName = n, groupApprovalId} -> do + getGroupAndReg groupId n >>= \case + Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." + Just (g, gr) -> + readTVarIO (groupRegStatus gr) >>= \case + GRSPendingApproval gaId + | gaId == groupApprovalId -> do + getDuplicateGroup g >>= \case + Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." + _ -> do + getGroupRolesStatus g gr >>= \case + Just GRSOk -> do + setGroupStatus st gr GRSActive + sendReply "Group approved!" + notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." + Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin + Just GRSContactNotOwner -> replyNotApproved "user is not an owner." + Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin + Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." + where + replyNotApproved reason = sendReply $ "Group is not approved: " <> reason + serviceNotAdmin = serviceName <> " is not an admin." + | otherwise -> sendReply "Incorrect approval code" + _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." + where + groupRef = groupReference' groupId n + DCRejectGroup _gaId _gName -> pure () + DCSuspendGroup groupId gName -> do + let groupRef = groupReference' groupId gName + getGroupAndReg groupId gName >>= \case + Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." + Just (_, gr) -> + readTVarIO (groupRegStatus gr) >>= \case + GRSActive -> do + setGroupStatus st gr GRSSuspended + notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators." + sendReply "Group suspended!" + _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." + DCResumeGroup groupId gName -> do + let groupRef = groupReference' groupId gName + getGroupAndReg groupId gName >>= \case + Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." + Just (_, gr) -> + readTVarIO (groupRegStatus gr) >>= \case + GRSSuspended -> do + setGroupStatus st gr GRSActive + notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!" + sendReply "Group listing resumed!" + _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." + DCListLastGroups count -> + readTVarIO (groupRegs st) >>= \grs -> do + sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "") + void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do + ct_ <- getContact cc dbContactId + let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ + sendGroupInfo ct gr dbGroupId $ Just ownerStr + DCExecuteCommand cmdStr -> + sendChatCmdStr cc cmdStr >>= \r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + sendReply $ serializeChatResponse (Just user) ts tz r + DCCommandError tag -> sendReply $ "Command error: " <> show tag + | otherwise = sendReply "You are not allowed to use this command" + where + superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} + sendReply = sendComposedMessage cc ct (Just ciId) . textMsgContent + + getGroupAndReg :: GroupId -> GroupName -> IO (Maybe (GroupInfo, GroupReg)) + getGroupAndReg gId gName = + getGroup cc gId + $>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} -> + if displayName == gName + then atomically (getGroupReg st gId) + $>>= \gr -> pure $ Just (g, gr) + else pure Nothing + + sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO () + sendGroupInfo ct gr@GroupReg {dbGroupId} useGroupId ownerStr_ = do + grStatus <- readTVarIO $ groupRegStatus gr + let statusStr = "Status: " <> groupRegStatusText grStatus + getGroupAndSummary cc dbGroupId >>= \case + Just (GroupInfo {groupProfile = p@GroupProfile {image = image_}}, GroupSummary {currentMembers}) -> do + let membersStr = "_" <> tshow currentMembers <> " members_" + text = T.unlines $ [tshow useGroupId <> ". " <> groupInfoText p] <> maybeToList ownerStr_ <> [membersStr, statusStr] + msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ + sendComposedMessage cc ct Nothing msg + Nothing -> do + let text = T.unlines $ [tshow useGroupId <> ". Error: getGroup. Please notify the developers."] <> maybeToList ownerStr_ <> [statusStr] + sendComposedMessage cc ct Nothing $ MCText text + +getContact :: ChatController -> ContactId -> IO (Maybe Contact) +getContact cc ctId = resp <$> sendChatCmd cc (APIGetChat (ChatRef CTDirect ctId) (CPLast 0) Nothing) + where + resp :: ChatResponse -> Maybe Contact + resp = \case + CRApiChat _ (AChat SCTDirect Chat {chatInfo = DirectChat ct}) -> Just ct + _ -> Nothing + +getGroup :: ChatController -> GroupId -> IO (Maybe GroupInfo) +getGroup cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) + where + resp :: ChatResponse -> Maybe GroupInfo + resp = \case + CRGroupInfo {groupInfo} -> Just groupInfo + _ -> Nothing + +getGroupAndSummary :: ChatController -> GroupId -> IO (Maybe (GroupInfo, GroupSummary)) +getGroupAndSummary cc gId = resp <$> sendChatCmd cc (APIGroupInfo gId) + where + resp = \case + CRGroupInfo {groupInfo, groupSummary} -> Just (groupInfo, groupSummary) + _ -> Nothing + +unexpectedError :: String -> String +unexpectedError err = "Unexpected error: " <> err <> ", please notify the developers." diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs new file mode 100644 index 0000000000..5082cab2ce --- /dev/null +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -0,0 +1,328 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Directory.Store + ( DirectoryStore (..), + GroupReg (..), + GroupRegStatus (..), + UserGroupRegId, + GroupApprovalId, + restoreDirectoryStore, + addGroupReg, + setGroupStatus, + setGroupRegOwner, + getGroupReg, + getUserGroupReg, + getUserGroupRegs, + filterListedGroups, + groupRegStatusText, + ) +where + +import Control.Concurrent.STM +import Control.Monad +import qualified Data.Attoparsec.ByteString.Char8 as A +import Data.ByteString.Char8 (ByteString) +import qualified Data.ByteString.Char8 as B +import Data.Composition ((.:)) +import Data.Int (Int64) +import Data.List (find, foldl', sortOn) +import Data.Map (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (isJust) +import Data.Set (Set) +import qualified Data.Set as S +import Data.Text (Text) +import Simplex.Chat.Types +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Util (ifM) +import System.IO (Handle, IOMode (..), openFile, BufferMode (..), hSetBuffering) +import System.Directory (renameFile, doesFileExist) + +data DirectoryStore = DirectoryStore + { groupRegs :: TVar [GroupReg], + listedGroups :: TVar (Set GroupId), + reservedGroups :: TVar (Set GroupId), + directoryLogFile :: Maybe Handle + } + +data GroupReg = GroupReg + { dbGroupId :: GroupId, + userGroupRegId :: UserGroupRegId, + dbContactId :: ContactId, + dbOwnerMemberId :: TVar (Maybe GroupMemberId), + groupRegStatus :: TVar GroupRegStatus + } + +data GroupRegData = GroupRegData + { dbGroupId_ :: GroupId, + userGroupRegId_ :: UserGroupRegId, + dbContactId_ :: ContactId, + dbOwnerMemberId_ :: Maybe GroupMemberId, + groupRegStatus_ :: GroupRegStatus + } + +type UserGroupRegId = Int64 + +type GroupApprovalId = Int64 + +data GroupRegStatus + = GRSPendingConfirmation + | GRSProposed + | GRSPendingUpdate + | GRSPendingApproval GroupApprovalId + | GRSActive + | GRSSuspended + | GRSSuspendedBadRoles + | GRSRemoved + +data DirectoryStatus = DSListed | DSReserved | DSRegistered + +groupRegStatusText :: GroupRegStatus -> Text +groupRegStatusText = \case + GRSPendingConfirmation -> "pending confirmation (duplicate names)" + GRSProposed -> "proposed" + GRSPendingUpdate -> "pending profile update" + GRSPendingApproval _ -> "pending admin approval" + GRSActive -> "active" + GRSSuspended -> "suspended by admin" + GRSSuspendedBadRoles -> "suspended because roles changed" + GRSRemoved -> "removed" + +grDirectoryStatus :: GroupRegStatus -> DirectoryStatus +grDirectoryStatus = \case + GRSActive -> DSListed + GRSSuspended -> DSReserved + GRSSuspendedBadRoles -> DSReserved + _ -> DSRegistered + +addGroupReg :: DirectoryStore -> Contact -> GroupInfo -> GroupRegStatus -> IO UserGroupRegId +addGroupReg st ct GroupInfo {groupId} grStatus = do + grData <- atomically addGroupReg_ + logGCreate st grData + pure $ userGroupRegId_ grData + where + addGroupReg_ = do + let grData = GroupRegData {dbGroupId_ = groupId, userGroupRegId_ = 1, dbContactId_ = ctId, dbOwnerMemberId_ = Nothing, groupRegStatus_ = grStatus} + gr <- dataToGroupReg grData + stateTVar (groupRegs st) $ \grs -> + let ugrId = 1 + foldl' maxUgrId 0 grs + grData' = grData {userGroupRegId_ = ugrId} + gr' = gr {userGroupRegId = ugrId} + in (grData', gr' : grs) + ctId = contactId' ct + maxUgrId mx GroupReg {dbContactId, userGroupRegId} + | dbContactId == ctId && userGroupRegId > mx = userGroupRegId + | otherwise = mx + +setGroupStatus :: DirectoryStore -> GroupReg -> GroupRegStatus -> IO () +setGroupStatus st gr grStatus = do + logGUpdateStatus st (dbGroupId gr) grStatus + atomically $ do + writeTVar (groupRegStatus gr) grStatus + updateListing st $ dbGroupId gr + where + updateListing = case grDirectoryStatus grStatus of + DSListed -> listGroup + DSReserved -> reserveGroup + DSRegistered -> unlistGroup + +setGroupRegOwner :: DirectoryStore -> GroupReg -> GroupMember -> IO () +setGroupRegOwner st gr owner = do + let memberId = groupMemberId' owner + logGUpdateOwner st (dbGroupId gr) memberId + atomically $ writeTVar (dbOwnerMemberId gr) (Just memberId) + +getGroupReg :: DirectoryStore -> GroupId -> STM (Maybe GroupReg) +getGroupReg st gId = find ((gId ==) . dbGroupId) <$> readTVar (groupRegs st) + +getUserGroupReg :: DirectoryStore -> ContactId -> UserGroupRegId -> STM (Maybe GroupReg) +getUserGroupReg st ctId ugrId = find (\r -> ctId == dbContactId r && ugrId == userGroupRegId r) <$> readTVar (groupRegs st) + +getUserGroupRegs :: DirectoryStore -> ContactId -> STM [GroupReg] +getUserGroupRegs st ctId = filter ((ctId ==) . dbContactId) <$> readTVar (groupRegs st) + +filterListedGroups :: DirectoryStore -> [(GroupInfo, GroupSummary)] -> STM [(GroupInfo, GroupSummary)] +filterListedGroups st gs = do + lgs <- readTVar $ listedGroups st + pure $ filter (\(GroupInfo {groupId}, _) -> groupId `S.member` lgs) gs + +listGroup :: DirectoryStore -> GroupId -> STM () +listGroup st gId = do + modifyTVar' (listedGroups st) $ S.insert gId + modifyTVar' (reservedGroups st) $ S.delete gId + +reserveGroup :: DirectoryStore -> GroupId -> STM () +reserveGroup st gId = do + modifyTVar' (listedGroups st) $ S.delete gId + modifyTVar' (reservedGroups st) $ S.insert gId + +unlistGroup :: DirectoryStore -> GroupId -> STM () +unlistGroup st gId = do + modifyTVar' (listedGroups st) $ S.delete gId + modifyTVar' (reservedGroups st) $ S.delete gId + +data DirectoryLogRecord + = GRCreate GroupRegData + | GRUpdateStatus GroupId GroupRegStatus + | GRUpdateOwner GroupId GroupMemberId + +data DLRTag = GRCreate_ | GRUpdateStatus_ | GRUpdateOwner_ + +logDLR :: DirectoryStore -> DirectoryLogRecord -> IO () +logDLR st r = forM_ (directoryLogFile st) $ \h -> B.hPutStrLn h (strEncode r) + +logGCreate :: DirectoryStore -> GroupRegData -> IO () +logGCreate st = logDLR st . GRCreate + +logGUpdateStatus :: DirectoryStore -> GroupId -> GroupRegStatus -> IO () +logGUpdateStatus st = logDLR st .: GRUpdateStatus + +logGUpdateOwner :: DirectoryStore -> GroupId -> GroupMemberId -> IO () +logGUpdateOwner st = logDLR st .: GRUpdateOwner + +instance StrEncoding DLRTag where + strEncode = \case + GRCreate_ -> "GCREATE" + GRUpdateStatus_ -> "GSTATUS" + GRUpdateOwner_ -> "GOWNER" + strP = + A.takeTill (== ' ') >>= \case + "GCREATE" -> pure GRCreate_ + "GSTATUS" -> pure GRUpdateStatus_ + "GOWNER" -> pure GRUpdateOwner_ + _ -> fail "invalid DLRTag" + +instance StrEncoding DirectoryLogRecord where + strEncode = \case + GRCreate gr -> strEncode (GRCreate_, gr) + GRUpdateStatus gId grStatus -> strEncode (GRUpdateStatus_, gId, grStatus) + GRUpdateOwner gId grOwnerId -> strEncode (GRUpdateOwner_, gId, grOwnerId) + strP = + strP >>= \case + GRCreate_ -> GRCreate <$> (A.space *> strP) + GRUpdateStatus_ -> GRUpdateStatus <$> (A.space *> A.decimal) <*> (A.space *> strP) + GRUpdateOwner_ -> GRUpdateOwner <$> (A.space *> A.decimal) <*> (A.space *> A.decimal) + +instance StrEncoding GroupRegData where + strEncode GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = + B.unwords + [ "group_id=" <> strEncode dbGroupId_, + "user_group_id=" <> strEncode userGroupRegId_, + "contact_id=" <> strEncode dbContactId_, + "owner_member_id=" <> strEncode dbOwnerMemberId_, + "status=" <> strEncode groupRegStatus_ + ] + strP = do + dbGroupId_ <- "group_id=" *> strP_ + userGroupRegId_ <- "user_group_id=" *> strP_ + dbContactId_ <- "contact_id=" *> strP_ + dbOwnerMemberId_ <- "owner_member_id=" *> strP_ + groupRegStatus_ <- "status=" *> strP + pure GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} + +instance StrEncoding GroupRegStatus where + strEncode = \case + GRSPendingConfirmation -> "pending_confirmation" + GRSProposed -> "proposed" + GRSPendingUpdate -> "pending_update" + GRSPendingApproval gaId -> "pending_approval:" <> strEncode gaId + GRSActive -> "active" + GRSSuspended -> "suspended" + GRSSuspendedBadRoles -> "suspended_bad_roles" + GRSRemoved -> "removed" + strP = + A.takeTill (\c -> c == ' ' || c == ':') >>= \case + "pending_confirmation" -> pure GRSPendingConfirmation + "proposed" -> pure GRSProposed + "pending_update" -> pure GRSPendingUpdate + "pending_approval" -> GRSPendingApproval <$> (A.char ':' *> A.decimal) + "active" -> pure GRSActive + "suspended" -> pure GRSSuspended + "suspended_bad_roles" -> pure GRSSuspendedBadRoles + "removed" -> pure GRSRemoved + _ -> fail "invalid GroupRegStatus" + +dataToGroupReg :: GroupRegData -> STM GroupReg +dataToGroupReg GroupRegData {dbGroupId_, userGroupRegId_, dbContactId_, dbOwnerMemberId_, groupRegStatus_} = do + dbOwnerMemberId <- newTVar dbOwnerMemberId_ + groupRegStatus <- newTVar groupRegStatus_ + pure + GroupReg + { dbGroupId = dbGroupId_, + userGroupRegId = userGroupRegId_, + dbContactId = dbContactId_, + dbOwnerMemberId, + groupRegStatus + } + +restoreDirectoryStore :: Maybe FilePath -> IO DirectoryStore +restoreDirectoryStore = \case + Just f -> ifM (doesFileExist f) (restore f) (newFile f >>= new . Just) + Nothing -> new Nothing + where + new = atomically . newDirectoryStore + newFile f = do + h <- openFile f WriteMode + hSetBuffering h LineBuffering + pure h + restore f = do + grs <- readDirectoryData f + renameFile f (f <> ".bak") + h <- writeDirectoryData f grs -- compact + atomically $ mkDirectoryStore h grs + +emptyStoreData :: ([GroupReg], Set GroupId, Set GroupId) +emptyStoreData = ([], S.empty, S.empty) + +newDirectoryStore :: Maybe Handle -> STM DirectoryStore +newDirectoryStore = (`mkDirectoryStore_` emptyStoreData) + +mkDirectoryStore :: Handle -> [GroupRegData] -> STM DirectoryStore +mkDirectoryStore h groups = + foldM addGroupRegData emptyStoreData groups >>= mkDirectoryStore_ (Just h) + where + addGroupRegData (!grs, !listed, !reserved) gr@GroupRegData {dbGroupId_ = gId} = do + gr' <- dataToGroupReg gr + let grs' = gr' : grs + pure $ case grDirectoryStatus $ groupRegStatus_ gr of + DSListed -> (grs', S.insert gId listed, reserved) + DSReserved -> (grs', listed, S.insert gId reserved) + DSRegistered -> (grs', listed, reserved) + +mkDirectoryStore_ :: Maybe Handle -> ([GroupReg], Set GroupId, Set GroupId) -> STM DirectoryStore +mkDirectoryStore_ h (grs, listed, reserved) = do + groupRegs <- newTVar grs + listedGroups <- newTVar listed + reservedGroups <- newTVar reserved + pure DirectoryStore {groupRegs, listedGroups, reservedGroups, directoryLogFile = h} + +readDirectoryData :: FilePath -> IO [GroupRegData] +readDirectoryData f = + sortOn dbGroupId_ . M.elems + <$> (foldM processDLR M.empty . B.lines =<< B.readFile f) + where + processDLR :: Map GroupId GroupRegData -> ByteString -> IO (Map GroupId GroupRegData) + processDLR m l = case strDecode l of + Left e -> m <$ putStrLn ("Error parsing log record: " <> e <> ", " <> B.unpack (B.take 80 l)) + Right r -> case r of + GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do + when (isJust $ M.lookup gId m) $ + putStrLn $ "Warning: duplicate group with ID " <> show gId <> ", group replaced." + pure $ M.insert gId gr m + GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of + Just gr -> pure $ M.insert gId gr {groupRegStatus_} m + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", status update ignored.") + GRUpdateOwner gId grOwnerId -> case M.lookup gId m of + Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", owner update ignored.") + +writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle +writeDirectoryData f grs = do + h <- openFile f WriteMode + hSetBuffering h LineBuffering + forM_ grs $ B.hPutStrLn h . strEncode . GRCreate + pure h diff --git a/blog/20230301-simplex-file-transfer-protocol.md b/blog/20230301-simplex-file-transfer-protocol.md index 08fec86adf..0008dd6b9b 100644 --- a/blog/20230301-simplex-file-transfer-protocol.md +++ b/blog/20230301-simplex-file-transfer-protocol.md @@ -4,6 +4,7 @@ title: "SimpleX File Transfer Protocol - a new protocol for sending large files date: 2023-03-01 preview: CLI and relays implementing the new XFTP protocol are released - you can use them now! image: images/20230301-xftp.jpg +imageWide: true permalink: "/blog/20230301-simplex-file-transfer-protocol.html" --- diff --git a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md index 93572dba33..0a66124934 100644 --- a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md +++ b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md @@ -63,7 +63,7 @@ To accelerate product development and growth we will be raising a seed funding t ### Send videos and files up to 1gb! -<img src="./images/20230422-video.png" width="288"> +<img src="./images/20230422-video.png" width="288" class="float-to-left"> In the beginning of March [we released servers and command-line utility to send and receive files via XFTP protocol](./20230301-simplex-file-transfer-protocol.md) - a very private and secure protocol that sends end-to-end encrypted files in chunks, protecting meta-data better than any alternatives we know of. @@ -88,7 +88,7 @@ Now you can choose whether to use faster and more convenient system biometric au ### Networking improvements -<img src="./images/20230422-socks.png" width="288"> +<img src="./images/20230422-socks.png" width="288" class="float-to-left"> Two small improvements to the app networking capabilities were added in this version. diff --git a/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md b/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md index e898763e1b..0cdbe2831f 100644 --- a/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md +++ b/blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md @@ -36,7 +36,7 @@ Also, we added Japanese and Portuguese (Brazil)<sup>*</sup> interface languages, ## Message reactions -<img src="./images/20230523-reactions.png" width="288"> +<img src="./images/20230523-reactions.png" width="288" class="float-to-left"> No idea why it took us so long to add them – finally we have them, and they are great. @@ -50,7 +50,7 @@ The next app version will allow prohibiting the reactions per conversation, as y ### Voice messages: up to 5 minutes, better quality, playback control -<img src="./images/20230523-voice.png" width="288"> +<img src="./images/20230523-voice.png" width="288" class="float-to-left"> Since [v4.3](./20221206-simplex-chat-v4.3-voice-messages.md#instant-voice-messages) voice messages were sent in small 16kb chunks, so we had to limit them to 30-40 seconds for better user experience, as sending larger files would require the sender to be online. @@ -66,7 +66,7 @@ This version allows to configure the time for messages to disappear more granula ### Message editing history -<img src="./images/20230523-info.png" width="288"> +<img src="./images/20230523-info.png" width="288" class="float-to-left"> I [wrote previously](./20221206-simplex-chat-v4.3-voice-messages.md#irreversible-message-deletion) why we decided to require the recipient concent before the messages can be fully deleted by the sender - in short, it is to support recipient's data sovereignty and prevent the possibility of offensive messages being removed without any trace. By default, when the sender deletes the message it is marked as deleted, rather than fully deleted, and you can reveal the original message. @@ -74,7 +74,7 @@ You've found the workaround for it of course - it's enough to simply edit the me ## Customize and share color themes -<img src="./images/20230523-theme.png" width="288"> +<img src="./images/20230523-theme.png" width="288" class="float-to-left"> Android app now allows choosing between three color themes - Light, Dark and SimpleX (a dark blue theme). You can customize any theme by setting 9 different colors used in the app, including titles, menus, accent colors and colors for sent and received messages. @@ -82,7 +82,7 @@ You can share your theme with other users by exporting it to a file and sending ## Self-destruct passcode -<img src="./images/20230523-self-destruct.png" width="288"> +<img src="./images/20230523-self-destruct.png" width="288" class="float-to-left"> This is something many of you asked before - when asked to enter the app passcode under duress, to be able to enter a special self-destruct code that would remove the app data. This feature is offered in many security tools, and now you can configure it in SimpleX Chat as well. diff --git a/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md b/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md index 42c08245f8..759587821b 100644 --- a/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md +++ b/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md @@ -38,7 +38,7 @@ permalink: "/blog/20230722-simplex-chat-v5-2-message-delivery-receipts.html" ### Message delivery receipts -<img src="./images/20230722-receipts.png" width="330"> +<img src="./images/20230722-receipts.png" width="330" class="float-to-left"> Most messaging apps add two ticks to sent messages – the first one to show that the message is accepted by the server, and the second – that it is delivered to the recipient's device. It confirms that the network is functioning, and that the message is not lost or delayed. SimpleX Chat now has this feature too! @@ -48,7 +48,7 @@ To avoid compromising your privacy, sending delivery receipts is disabled for al ### Filter favorite and unread chats -<img src="./images/20230722-filter.png" width="288"> +<img src="./images/20230722-filter.png" width="288" class="float-to-left"> You can now mark your contacts and groups as _favorite_, to be able to find them faster. With filter enabled, you will only see favorite chats, chats that contain unread messages and also any unaccepted group invitations and contact requests. @@ -58,13 +58,13 @@ Active SimpleX Chat users know how broken the current group experience is, and t #### What is this in reply to? -<img src="./images/20230722-quoted.png" width="330"> +<img src="./images/20230722-quoted.png" width="330" class="float-to-left"> A major problem is that you can see replies to the messages you've not seen before - this would happen both when you just join the group, and didn't connect to most other members, and also when other new members join the group and they didn't yet connect to you – so literally all the time, and the bigger the group gets, the worse it becomes. While this problem cannot be solved without major group protocol changes, at least there is now ability to see the original message that was replied to via the message information. #### How to connect to this member? -<img src="./images/20230722-search.png" width="330"> +<img src="./images/20230722-search.png" width="330" class="float-to-left"> To simplify direct connections with other group members, you can now share your SimpleX address via your chat profile, and group members can send you a contact request even if the group does not allow direct messages. diff --git a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md new file mode 100644 index 0000000000..2aa8a7bc0c --- /dev/null +++ b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md @@ -0,0 +1,131 @@ +--- +layout: layouts/article.html +title: "SimpleX Chat v5.3 released: desktop app, local file encryption and improved groups with directory service" +date: 2023-09-25 +image: /docs/images/simplex-desktop-light.png +imageWide: true +previewBody: blog_previews/20230925.html +permalink: "/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html" +--- + +# SimpleX Chat v5.3 released: desktop app, local file encryption and improved groups + +**Published:** September 25, 2023 + +**What's new in v5.3:** +- [new desktop app!](#multiplatform-desktop-app)! +- [directory service and other group improvements](#group-directory-service-and-other-group-improvements). +- [encrypted local files and media with forward secrecy](#encrypted-local-files-and-media-with-forward-secrecy). +- [simplified incognito mode](#simplified-incognito-mode). + +There are a lot of other improvements and fixes in this release: +- improved app responsiveness and stability. +- app memory usage is reduced by 40%. +- new privacy settings: show last messages & save draft. +- fixes: + - bug preventing group members connecting (it will only help the new connections). + - playing videos on full screen<sup>**</sup>. + - screen reader for messages<sup>**</sup>. + - reduced background crashes<sup>**</sup>. + +Also, we added 6 new interface languages: Arabic<sup>*</sup>, Bulgarian, Finnish, Hebrew<sup>*</sup>, Thai and Ukrainian - thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +\* Android app. + +\*\* iOS app. + +## Multiplatform desktop app + +<img src="/docs/images/simplex-desktop-light.png" width="640"> + +Thanks a lot to everybody who was testing the desktop app since July – it really helped to make it stable! + +To use desktop app you need to **create a new profile**. As SimpleX platform has no user accounts, it's not as simple as for centralized apps to access the same profile from two devices. + +The next app version will allow using your mobile profile from desktop app. For now, as a workaround, you can join groups from both mobile and desktop devices, and use small groups instead of direct conversations. + +When you start the app first time, you will be offered to **set database passphrase** – you have to memorize it, as there is no way to recover it. If you skip it, a random passphrase will be generated and stored on your desktop device as plaintext (unencrypted) – you can change it later. + +Other limitations of the desktop app: +- you cannot send voice messages. +- there is no support for calls yet. + +## Group directory service and other group improvements + +<img src="./images/20230925-directory.png" width="330" class="float-to-left"> + +Directory service provides a way to search for public groups submitted by the users. To use it, you need to connect to it via SimpleX Chat, as you would connect to any other contact, and type some words to search. + +You can also create and register your group, with some limitations explained [here](../docs/DIRECTORY.md). + +Other group improvements in this release: + +- you can send delivery receipts to the groups up to 20 members. + +- if the group settings allow it, you can send direct messages to group members even after you deleted the contact. + +- connections between members are made faster, and the bug that prevented the connections in some cases is fixed in this release. + +The next release will reduce the time it takes to send messages to the group, especially when there are many members or when you have a slow device storage. + +## Encrypted local files and media with forward secrecy + +<img src="./images/20230925-encrypted.png" width="330" class="float-to-left"> + +All messages, files and media sent via SimpleX Chat were always end-to-end encrypted from the very beginning. SimpleX Chat uses double-ratchet algorithm with encrypted message headers, for the best possible meta-data protection. + +You contacts, groups and messages are stored in the local database on your device, and this database was encrypted from [v4.0 released a year ago](./20220928-simplex-chat-v4-encrypted-database.md). + +But until this version all files and media in the app storage were not encrypted, and when you exported the chat archive, they were unencrypted there as well. + +From v5.3 all files and media (except videos, for now) are encrypted with a random symmetric key - in many cases they are encrypted before they are written to the storage. Local file encryption can be disabled via Privacy & Security settings, for example, if you need to access the files from the storage outside of the app. + +In addition to the videos that are stored unencrypted, there are other rare scenarios when the received files may be unencrypted in this release. Files have an open or closed lock icons to indicate whether they were encrypted locally. These limitations will be addressed in the next release. In any case, all files and media are always sent end-to-end encrypted, without any exceptions. + +The keys used to encrypt files locally are associated with the messages and stored in the encrypted database. If you delete a message with the attached file or media, the key will be irreversibly deleted as well. Even if an attacker gains access to your database passphrase later and to the copy of the encrypted file, they won't be able to decrypt the file. + +This approach provides forward secrecy for locally stored files, unlike file encryption schemes used in some other apps when the same passphrase is used for all files. + +## Simplified incognito mode + +<img src="./images/20230925-incognito.png" width="330" class="float-to-left"> + +Incognito mode was [added a year ago](./20220901-simplex-chat-v3.2-incognito-mode.md) to improve anonymity of your profile, but it was confusing for some users - it was a global setting, but it only affected the new connections. + +It is now simpler to use - you can decide whether to connect to a contact or join a group using your main profile at a point when you create an invitation link or connect via a link or QR code. + +When you are connecting to people your know you usually want to share your main profile, and when connecting to public groups or strangers, you may prefer to use a random profile. + +## SimpleX platform + +Some links to answer the most common questions: + +[SimpleX Chat security assessment](./20221108-simplex-chat-v4.2-security-audit-new-website.md). + +[How can SimpleX deliver messages without user identifiers](https://simplex.chat/#how-simplex-works). + +[What are the risks to have identifiers assigned to the users](https://simplex.chat/#why-ids-bad-for-privacy). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). + +[How SimpleX is different from Session, Matrix, Signal, etc.](https://github.com/simplex-chat/simplex-chat/blob/stable/README.md#frequently-asked-questions). + +Visit our [website](https://simplex.chat) to learn more. + +## Help us with donations + +Huge thank you to everybody who donated to SimpleX Chat! + +We are prioritizing users privacy and security - it would be impossible without your support. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/images/20230925-directory.png b/blog/images/20230925-directory.png new file mode 100644 index 0000000000..d5a2f26683 Binary files /dev/null and b/blog/images/20230925-directory.png differ diff --git a/blog/images/20230925-encrypted.png b/blog/images/20230925-encrypted.png new file mode 100644 index 0000000000..0c482264bc Binary files /dev/null and b/blog/images/20230925-encrypted.png differ diff --git a/blog/images/20230925-incognito.png b/blog/images/20230925-incognito.png new file mode 100644 index 0000000000..949f36e747 Binary files /dev/null and b/blog/images/20230925-incognito.png differ diff --git a/cabal.project b/cabal.project index b747dff8be..7c8886d94a 100644 --- a/cabal.project +++ b/cabal.project @@ -2,12 +2,14 @@ packages: . -- packages: . ../simplexmq -- packages: . ../simplexmq ../direct-sqlcipher ../sqlcipher-simple +with-compiler: ghc-8.10.7 + constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: fdbfe0e8d159f394f6eb0f5168620da8694033cd + tag: 53c793d5590d3c781aa3fbf72993eee262c7aa83 source-repository-package type: git diff --git a/docs/CLI.md b/docs/CLI.md index 65de29e601..7966627c4a 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -16,7 +16,7 @@ revision: 31.01.2023 - [Windows](#windows) - [Build from source](#build-from-source) - [Using Docker](#using-docker) - - [Using Haskell stack](#using-haskell-stack) + - [Using Haskell in any OS](#in-any-os) - [Usage](#usage) - [Running the chat client](#running-the-chat-client) - [Access messaging servers via Tor](#access-messaging-servers-via-tor-beta) @@ -102,27 +102,49 @@ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . #### In any OS -1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 8.10.7 and cabal: +1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.2 and cabal 3.10.1.0: ```shell curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh ``` -2. Build the project: +You can use `ghcup tui` to check or add GHC and cabal versions. + +2. Clone the source code: ```shell git clone git@github.com:simplex-chat/simplex-chat.git cd simplex-chat git checkout stable -# on Linux +# or to build a specific version: +# git checkout v5.3.0-beta.8 +``` + +`master` is a development branch, it may containt unstable code. + +3. Prepare the system: + +On Linux: + +```shell apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev cp scripts/cabal.project.local.linux cabal.project.local -# or on MacOS: -# brew install openssl@1.1 -# cp scripts/cabal.project.local.mac cabal.project.local -# you may need to amend cabal.project.local to point to the actual openssl location +``` + +On Mac: + +``` +brew install openssl@1.1 +cp scripts/cabal.project.local.mac cabal.project.local +``` + +You may need to amend cabal.project.local to point to the actual openssl location. + +4. Build the app: + +```shell cabal update -cabal install +cabal install simplex-chat ``` ## Usage diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ef58bfec04..dc18bebb02 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -24,3 +24,88 @@ MacOS comes with LibreSSL as default, OpenSSL must be installed to compile Simpl OpenSSL can be installed with `brew install openssl@1.1` You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order to have things working properly + + +## Project branches + +**In simplex-chat repo** + +- `stable` - stable release of the apps, can be used for updates to the previous stable release (GHC 9.6.2). + +- `stable-android` - used to build stable Android core library with Nix (GHC 8.10.7). + +- `stable-ios` - used to build stable iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `stable-android` except Nix configuration files. + +- `master` - branch for beta version releases (GHC 9.6.2). + +- `master-android` - used to build beta Android core library with Nix (GHC 8.10.7). + +- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `master-android` except Nix configuration files. + +**In simplexmq repo** + +- `master` - uses GHC 9.6.2 its commit should be used in `master` branch of simplex-chat repo. + +- `master-ghc8107` - its commit should be used in `master-android` (and `master-ios`) branch of simplex-chat repo. + +## Development & release process + +1. Make PRs to `master` branch _only_ for both simplex-chat and simplexmq repos. + +2. If simplexmq repo was changed, to build mobile core libraries you need to merge its `master` branch into `master-ghc8107` branch. + +3. To build Android core library: +- merge `master` branch to `master-android` branch. +- update code to be compatible with GHC 8.10.7 (see below). +- update `simplexmq` commit in `master-android` branch to the commit in `master-ghc8107` branch. +- push to GitHub. + +4. To build iOS core library, merge `master-android` branch to `master-ios` branch, and push to GitHub. + +5. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. + +6. After the public release to App Store and Play Store, merge: +- `master` to `stable` +- `master` to `master-android` (and compile/update code) +- `master-android` to `master-ios` +- `master-android` to `stable-android` +- `master-ios` to `stable-ios` + +7. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. + + +## Differences between GHC 8.10.7 and GHC 9.6.2 + +1. The main difference is related to `DuplicateRecordFields` extension. + +It is no longer possible in GHC 9.6.2 to specify type when using selectors, instead OverloadedRecordDot extension and syntax are used that need to be removed in GHC 8.10.7: + +```haskell +{-# LANGUAGE DuplicateRecordFields #-} +-- use this in GHC 9.6.2 when needed +{-# LANGUAGE OverloadedRecordDot #-} + +-- GHC 9.6.2 syntax +let x = record.field + +-- GHC 8.10.7 syntax removed in GHC 9.6.2 +let x = field (record :: Record) +``` + +It is still possible to specify type when using record update syntax, use this pragma to suppress compiler warning: + +```haskell +-- use this in GHC 9.6.2 when needed +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +let r' = (record :: Record) {field = value} +``` + +2. Most monad functions now have to be imported from `Control.Monad`, and not from specific monad modules (e.g. `Control.Monad.Except`). + +```haskell +-- use this in GHC 9.6.2 when needed +import Control.Monad +``` + +[This PR](https://github.com/simplex-chat/simplex-chat/pull/2975/files) has all the differences. diff --git a/docs/DIRECTORY.md b/docs/DIRECTORY.md index 2370d46aef..a55f4fc510 100644 --- a/docs/DIRECTORY.md +++ b/docs/DIRECTORY.md @@ -1,3 +1,8 @@ +--- +title: SimpleX Directory Service +revision: 18.08.2023 +--- + # SimpleX Directory Service You can use an experimental directory service to discover the groups created and registered by other users. @@ -10,9 +15,10 @@ Please note that your search queries can be kept by the bot as the conversation ## Adding groups to the directory -### How to add the group -To add the group you must be the owner of the group. Once you connect to the directory service and send `/help`, the service will guide you through the process. +### How to add a group + +To add a group you must be its owner. Once you connect to the directory service and send `/help`, the service will guide you through the process. 1. Invite SimpleX Service Directory to the group as `admin` member. You can also set the role to `admin` after inviting the directory service. diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md new file mode 100644 index 0000000000..83fa18f97d --- /dev/null +++ b/docs/DOWNLOADS.md @@ -0,0 +1,42 @@ +--- +title: Download SimpleX apps +permalink: /downloads/index.html +revision: 20.09.2023 +--- + +| Updated 20.09.2023 | Languages: EN | +# Download SimpleX apps + +The latest version is v5.3. + +- [desktop](#desktop-app) +- [mobile](#mobile-apps) +- [terminal](#terminal-console-app) (console) + +## Desktop app + +<img src="/docs/images/simplex-desktop-light.png" alt="desktop app" width=500> + +Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps. + +**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-ubuntu-22_04-x86_64.deb). + +**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). + +**Windows**: coming soon. + +## Mobile apps + +**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084) (v5.2.3), [TestFlight](https://testflight.apple.com/join/DWuT2LQu) (v5.3-beta.9). + +**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-armv7a.apk). + +## Terminal (console) app + +See [Using terminal app](/docs/CLI.md). + +**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-ubuntu-22_04-x86-64). + +**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). + +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0/simplex-chat-windows-x86-64). diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index c1f9e6c014..5a31b3e058 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -1,3 +1,9 @@ +--- +title: Join SimpleX Chat team +permalink: /jobs/index.html +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. @@ -7,35 +13,6 @@ We currently have 4 full-time people in the team - all engineers, including the We want to add up to 3 people to the team. -**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. - -- **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. - -- **Love software engineering**: - - have 5y+ of software engineering experience in complex projects, - - great understanding of the common principles: - - data structures, bits and byte manipulation - - text encoding and manipulation - - software design and algorithms - - concurrency - - networking - -- **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. - - ## Who we are looking for ### Systems Haskell engineer @@ -63,6 +40,35 @@ You are a product UX expert who designs great user experiences directly in iOS c Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps. +## 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. + +- **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. + +- **Love software engineering**: + - have 5y+ of software engineering experience in complex projects, + - great understanding of the common principles: + - data structures, bits and byte manipulation + - text encoding and manipulation + - software design and algorithms + - concurrency + - networking + +- **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. + + ## How to join the team 1. [Install the app](../README.md#install-the-app), try using it with the friends and [join some user groups](https://github.com/simplex-chat/simplex-chat#join-user-groups) – you will discover a lot of things that need improvements. diff --git a/docs/SERVER.md b/docs/SERVER.md index e440de944f..00e3e0f6ee 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -1,6 +1,6 @@ --- title: Hosting your own SMP Server -revision: 05.06.2023 +revision: 31.07.2023 --- | Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md) | @@ -19,17 +19,27 @@ _Please note_: when you change the servers in the app configuration, it only aff 0. First, install `smp-server`: - - Manual deployment: + - Manual deployment (see below) - - [Compiling from source](https://github.com/simplex-chat/simplexmq#using-your-distribution) - - [Using pre-compiled binaries](https://github.com/simplex-chat/simplexmq#install-binaries) - - - Alternatively, you can deploy `smp-server` using: - - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker-1) + - Semi-automatic deployment: + - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) - [Linode StackScript](https://github.com/simplex-chat/simplexmq#deploy-smp-server-on-linode) Manual installation requires some preliminary actions: +0. Install binary: + + - Using offical binaries: + + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server + ``` + + - Compiling from source: + + Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution) + 1. Create user and group for `smp-server`: ```sh @@ -57,24 +67,104 @@ Manual installation requires some preliminary actions: ```sh [Unit] - Description=SMP server + Description=SMP server systemd service + [Service] User=smp Group=smp Type=simple - ExecStart=smp-server start + ExecStart=/usr/local/bin/smp-server start +RTS -N -RTS ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex/smp-server-store.log" ] && cp "/var/opt/simplex/smp-server-store.log" "/var/opt/simplex/smp-server-store.log.bak"' + LimitNOFILE=65535 KillSignal=SIGINT TimeoutStopSec=infinity - Restart=always - RestartSec=10 - LimitNOFILE=65535 + [Install] WantedBy=multi-user.target ``` And execute `sudo systemctl daemon-reload`. +## Tor installation + +smp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user. + +1. Install tor: + + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. + + - Configure offical Tor PPA repository: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list + ``` + + - Import repository key: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null + ``` + + - Update repository index: + + ```sh + apt update + ``` + + - Install `tor` package: + + ```sh + apt install -y tor deb.torproject.org-keyring + ``` + +2. Configure tor: + + - File configuration: + + Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.): + + ```sh + vim /etc/tor/torrc + ``` + + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + + ```sh + # Enable log (otherwise, tor doesn't seemd to deploy onion address) + Log notice file /var/log/tor/notices.log + # Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside smp-server and onion address will be displayed in clients, this is totally fine) + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # smp-server hidden service host directory and port mappings + HiddenServiceDir /var/lib/tor/simplex-smp/ + HiddenServicePort 5223 localhost:5223 + ``` + + - Create directories: + + ```sh + mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ + ``` + +3. Start tor: + + Enable `systemd` service and start tor. Offical `tor` is a bit flunky on the first start and may not create onion host address, so we're restarting it just in case. + + ```sh + systemctl enable tor && systemctl start tor && systemctl restart tor + ``` + +4. Display onion host: + + Execute the following command to display your onion host address: + + ```sh + cat /var/lib/tor/simplex-smp/hostname + ``` + ## Configuration To see which options are available, execute `smp-server` without flags: diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index 79161e977e..2977ff15da 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -1,6 +1,6 @@ --- title: Hosting your own XFTP Server -revision: 21.04.2023 +revision: 31.07.2023 --- # Hosting your own XFTP Server @@ -17,26 +17,43 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba ## Installation -1. Download `xftp-server` binary: +0. First, install `xftp-server`: - ```sh - sudo curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server && sudo chmod +x /usr/local/bin/xftp-server - ``` + - Manual deployment (see below) -2. Create user and group for `xftp-server`: + - Semi-automatic deployment: + - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) + +Manual installation requires some preliminary actions: + +0. Install binary: + + - Using offical binaries: + + ```sh + curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/xftp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/xftp-server + ``` + + - Compiling from source: + + Please refer to [Build from source: Using your distribution](https://github.com/simplex-chat/simplexmq#using-your-distribution) + + +1. Create user and group for `xftp-server`: ```sh sudo useradd -m xftp ``` -3. Create necessary directories and assign permissions: +2. Create necessary directories and assign permissions: ```sh sudo mkdir -p /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp sudo chown xftp:xftp /var/opt/simplex-xftp /etc/opt/simplex-xftp /srv/xftp ``` -4. Allow xftp-server port in firewall: +3. Allow xftp-server port in firewall: ```sh # For Ubuntu @@ -46,7 +63,7 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba sudo firewall-cmd --reload ``` -5. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content: +4. **Optional** — If you're using distribution with `systemd`, create `/etc/systemd/system/xftp-server.service` file with the following content: ```sh [Unit] @@ -69,6 +86,86 @@ XFTP is a new file transfer protocol focussed on meta-data protection - it is ba And execute `sudo systemctl daemon-reload`. +## Tor installation + +xftp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user. + +1. Install tor: + + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. + + - Configure offical Tor PPA repository: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list + ``` + + - Import repository key: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null + ``` + + - Update repository index: + + ```sh + apt update + ``` + + - Install `tor` package: + + ```sh + apt install -y tor deb.torproject.org-keyring + ``` + +2. Configure tor: + + - File configuration: + + Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.): + + ```sh + vim /etc/tor/torrc + ``` + + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + + ```sh + # Enable log (otherwise, tor doesn't seemd to deploy onion address) + Log notice file /var/log/tor/notices.log + # Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside xftp-server and onion address will be displayed in clients, this is totally fine) + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # xftp-server hidden service host directory and port mappings + HiddenServiceDir /var/lib/tor/simplex-xftp/ + HiddenServicePort 443 localhost:443 + ``` + + - Create directories: + + ```sh + mkdir /var/lib/tor/simplex-xftp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-xftp/ && chmod 700 /var/lib/tor/simplex-xftp/ + ``` + +3. Start tor: + + Enable `systemd` service and start tor. Offical `tor` is a bit flunky on the first start and may not create onion host address, so we're restarting it just in case. + + ```sh + systemctl enable tor && systemctl start tor && systemctl restart tor + ``` + +4. Display onion host: + + Execute the following command to display your onion host address: + + ```sh + cat /var/lib/tor/simplex-xftp/hostname + ``` + ## Configuration To see which options are available, execute `xftp-server` without flags: diff --git a/docs/images/simplex-desktop-dark-1.png b/docs/images/simplex-desktop-dark-1.png new file mode 100644 index 0000000000..c0b441f623 Binary files /dev/null and b/docs/images/simplex-desktop-dark-1.png differ diff --git a/docs/images/simplex-desktop-dark-2.png b/docs/images/simplex-desktop-dark-2.png new file mode 100644 index 0000000000..2a9d9b8fe2 Binary files /dev/null and b/docs/images/simplex-desktop-dark-2.png differ diff --git a/docs/images/simplex-desktop-light.png b/docs/images/simplex-desktop-light.png new file mode 100644 index 0000000000..d28b7b88ca Binary files /dev/null and b/docs/images/simplex-desktop-light.png differ diff --git a/docs/protocol/diagrams/group.mmd b/docs/protocol/diagrams/group.mmd index c331b46100..18d392caa5 100644 --- a/docs/protocol/diagrams/group.mmd +++ b/docs/protocol/diagrams/group.mmd @@ -3,24 +3,31 @@ sequenceDiagram participant A as Alice participant B as Bob participant C as Existing<br>contact - + note over A, B: 1. send and accept group invitation A ->> B: x.grp.inv<br>invite Bob to group<br>(via contact connection) - B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection) - B ->> A: establish group member connection + B ->> A: x.grp.acpt<br>accept invitation<br>(via member connection)<br>establish group member connection note over M, B: 2. introduce new member Bob to all existing members A ->> M: x.grp.mem.new<br>"announce" Bob<br>to existing members<br>(via member connections) - A ->> B: x.grp.mem.intro * N<br>"introduce" members<br>(via member connection) - B ->> A: x.grp.mem.inv * N<br>"invitations" to connect<br>for all members<br>(via member connection) - A ->> M: x.grp.mem.fwd<br>forward "invitations"<br>to all members<br>(via member connections) + loop batched + A ->> B: x.grp.mem.intro * N<br>"introduce" members and<br>their chat protocol versions<br>(via member connection) + note over B: prepare group member connections + opt chat protocol compatible version < 2 + note over B: prepare direct connections + end + B ->> A: x.grp.mem.inv * N<br>"invitations" to connect<br>for all members<br>(via member connection) + end + A ->> M: x.grp.mem.fwd<br>forward "invitations" and<br>Bob's chat protocol version<br>to all members<br>(via member connections) note over M, B: 3. establish direct and group member connections M ->> B: establish group member connection - M ->> B: establish direct connection - note over M, C: 4. deduplicate new contact - B ->> M: x.info.probe<br>"probe" is sent to all new members - B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match - C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user - note over B: merge existing and new contacts if received and sent probe hashes match + opt chat protocol compatible version < 2 + M ->> B: establish direct connection + note over M, C: 4. deduplicate new contact + B ->> M: x.info.probe<br>"probe" is sent to all new members + B ->> C: x.info.probe.check<br>"probe" hash,<br>in case contact and<br>member profiles match + C ->> B: x.info.probe.ok<br> original "probe",<br> in case contact and member<br>are the same user + note over B: merge existing and new contacts if received and sent probe hashes match + end diff --git a/docs/protocol/diagrams/group.svg b/docs/protocol/diagrams/group.svg index d66b560b21..8c1b65dee2 100644 --- a/docs/protocol/diagrams/group.svg +++ b/docs/protocol/diagrams/group.svg @@ -1 +1 @@ -<svg aria-labelledby="chart-title-graph-div chart-desc-graph-div" role="img" viewBox="-50 -10 1125.5 1328" style="max-width: 1125.5px;" height="1328" xmlns="http://www.w3.org/2000/svg" width="100%" id="graph-div" xmlns:xlink="http://www.w3.org/1999/xlink"><style>@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css");'</style><title id="chart-title-graph-div">N existingmembersAliceBobExistingcontact1. send and accept group invitation2. introduce new member Bob to all existing members3. establish direct and group member connections4. deduplicate new contactmerge existing and new contacts if received and sent probe hashes matchx.grp.invinvite Bob to group(via contact connection)x.grp.acptestablish group member connectionx.grp.mem.new"announce" Bobto existing members(via member connections)x.grp.mem.intro * N"introduce" members(via member connection)x.grp.mem.inv * N"invitations" to connectfor all members(via member connection)x.grp.mem.fwdforward "invitations"to all members(via member connections)establish group member connectionestablish direct connectionx.info.probe"probe" is sent to all new membersx.info.probe.check"probe" hash,in case contact andmember profiles matchx.info.probe.ok original "probe", in case contact and memberare the same userN existingmembersAliceBobExistingcontact \ No newline at end of file +ExistingcontactBobAliceN existingmembersExistingcontactBobAliceN existingmembers1. send and accept group invitation2. introduce new member Bob to all existing membersprepare group member connectionsprepare direct connectionsopt[chat protocolcompatible version< 2]loop[batched]3. establish direct and group member connections4. deduplicate new contactmerge existing and new contacts if received and sent probe hashes matchopt[chat protocol compatible version < 2]x.grp.invinvite Bob to group(via contact connection)x.grp.acptaccept invitation(via member connection)establish group member connectionx.grp.mem.new"announce" Bobto existing members(via member connections)x.grp.mem.intro * N"introduce" members andtheir chat protocol versions(via member connection)x.grp.mem.inv * N"invitations" to connectfor all members(via member connection)x.grp.mem.fwdforward "invitations" andBob's chat protocol versionto all members(via member connections)establish group member connectionestablish direct connectionx.info.probe"probe" is sent to all new membersx.info.probe.check"probe" hash,in case contact andmember profiles matchx.info.probe.ok original "probe", in case contact and memberare the same user \ No newline at end of file diff --git a/docs/rfcs/2023-04-28-files-encryption.md b/docs/rfcs/2023-04-28-files-encryption.md new file mode 100644 index 0000000000..30c6a4d2dd --- /dev/null +++ b/docs/rfcs/2023-04-28-files-encryption.md @@ -0,0 +1,64 @@ +# Encrpting local app files + +## Problem + +Currently, the files are stored in the file storage unencrypted, unlike the database. + +There are multiple operations in the app that access files: + +1. Sending files via SMP - chat core reads the files chunk by chunk and sends them. The file can be encrypted once sent and the "encrypted" flag added. + +2. Sending files via XFTP - simplexmq encrypts the file first and then sends it. Currently, we are deleting the file from chat, once its uploaded, there is no reason to keep unencrypted file (from XFTP point of view) once its encrypted. + +3. Viewing images in the mobile apps. + +4. Playing voice files in the mobile apps. + +5. Playing videos and showing video previews in mobile apps. + +6. Saving files from the app storage to the device. + +## Possible solutions + +### System encryption + +A possible approach is to use platform-specific encryption mechanism. The problem with that approach is inconsistency between platforms, and that the files in chat archive will probably be unencrypted in this case. + +### App encryption + +Files will be encrypted once received, using storage key, and the core would expose C apis to mobile apps: + +1. Read the file with decryption - this can be used for image previews, for example, as a replacement for OS file reading. + +2. Copy the file with decryption to some permanent destination - this can be used for saving files to the device. + +3. Copy the file into a temporary location with decryption - this can be used for playing voice/video files. The app would remove the files once no longer used, and this temporary location can be cleaned on each app start, to clean up the files that the app failed to remove. Alternative to that would be to have both encrypted and decrypted copies available for the file, with paths stored in the database, and clean up process removed decrypted copies once no longer used - there should be some flags to indicate when decrypted copy can be deleted. + +For specific use cases: + +1. Viewing images in the mobile apps. + - iOS: we use `UIImage(contentsOfFile path: String)`. We could use `init?(data: Data)` instead, and decrypt the file in memory before passing it to the image view. Images are small enough for this approach to be ok, and in any case the image is read to memory as a whole. + - Android: we use `BitmapFactory.decodeFileDescriptor` (?). We could use ... + +2. Playing voice files in the mobile apps. + - iOS: we use `AVAudioPlayer.init(contentsOf: URL)` to play the file. We could either decrypt the file before playing it, or, given that voice files are small (even if we increase allowed duration, they are still likely to be under 1mb), we could use `init(data: Data)` to avoid creating decrypted file. + - Android: we use `MediaPlayer.setDataSource(filePath)`. We could use ... + +3. Showing video previews. + - iOS: ... + - Android: ... + + Possibly, we will need to store preview as a separate file, to avoid decrypting the whole video just to show preview. + +4. Playing video files. + - iOS: we use `AVPlayer(url: URL)`, the file will have to be decrypted for playback. + - Android: ... + +5. Saving files from the app storage to the device. + The file will have to be decrypted, passed to the system, and then decrypted copy deleted once no longer needed. + +### Which key to use for encryption + +1. Derive file encryption key from database storage key. The downside for this approach is managing key changes - they will be slow. Also, if file encryption is made optional, and in any case, for the existing users all files are not encrypted yet, we will need somehow to track which files are encrypted. + +2. Random per-file encryption key stored in the database. Given that the database is already encrypted, it can be a better approach, and it makes it easier to manage file encryption/decryption. File keys will not be sent to the client application, but they will be accessible via the database queries of course. diff --git a/docs/rfcs/2023-07-25-contact-groups.md b/docs/rfcs/2023-07-25-contact-groups.md new file mode 100644 index 0000000000..63dfce4b80 --- /dev/null +++ b/docs/rfcs/2023-07-25-contact-groups.md @@ -0,0 +1,55 @@ +# Contact groups + +## Problem + +Bad UX when communicating from multiple devices: +- Lack of data synchronization between devices. +- Participating in the same conversation from multiple devices requires manually adding each device to a group. +- For a direct (1-to-1) conversation, it has to be initially created as a group - a contact does not support adding devices to conversation. Not obvious use of groups. + +## Solution + +Full device synchronization is a complex feature and will not be discussed in this rfc. Instead a proposed solution is to support adding devices to a 1-to-1 conversation, using existing group technology. + +Pros: +- Fully decentralized, no leading and following devices. +- No complex full state synchronization. +- More obvious UX than using groups for 1-to-1 conversations. +- Can be automated to include user devices into all \[new\] conversations. + +Cons: +- Same downsides groups have - increased traffic, diverging group state, possible failures to establish connections (requires out-of-band "connection doctor"?). +- Muddies existing abstracts of contacts and groups. Adds new orthogonal complexity to many existing features - preferences, incognito, multi-profile, calls, etc. +- Requires cooperation of contact's client - to support new protocol message for contact-to-group upgrade / contact-group invitation link. +- Still not full data synchronization. Devices have different profiles and likely different lists of conversations. + +### Possible approaches + +1. Add device to contact conversation - upgrade contact to group. Optionally automate adding user devices to other conversations. +2. Managed list of user devices - non optionally automate adding to other conversations. + +#### Upgrade contact to group + +Pros: +- Allows to migrate existing contacts to multi-device conversation. + +Cons: +- Button would be hidden in chat info. +- Requires upgrading contact to group on both sides of the conversation: + - Replace contact with group, update entities - chat items, files, etc. How will files in progress migrate? + - New protocol message for contact to perform same upgrade + introduction of new device. +- Implicit list of "my devices", unclear whether to automate adding devices for other contacts? Not as useful if it's not automated? + +#### Managed list of user devices + +Pros: +- New button in Settings - easier to discover than chat info button. +- User devices are automatically added to all new conversations. +- Start each conversation as a group? -> Doesn't require upgrading contact to group. + - New invitation link type - contact-group. + - Expanded info in confirmation (when invitation link is for regular contact but joining client has other devices). +- Can automatically add own devices to existing groups. +- Doesn't require tricky contact to group upgrade. + +Cons: +- Cannot migrate existing contacts. Create all contacts going forward as contact-groups? diff --git a/docs/rfcs/2023-08-10-groups-wt-contacts.md b/docs/rfcs/2023-08-10-groups-wt-contacts.md new file mode 100644 index 0000000000..6060337915 --- /dev/null +++ b/docs/rfcs/2023-08-10-groups-wt-contacts.md @@ -0,0 +1,75 @@ +# Create groups without establishing direct connections + +## Problems + +Current groups design involves creating two connections per member - a group and a direct connection (for member's contact), even though current setting for allowing direct messages between members is off by default. This leads to double the traffic on subscriptions and especially inefficient in large groups. + +Another problem is that after deleting member's contact there is no way to re-establish direct connection with member, except out-of-band. + +See also [Group contacts management](./2022-10-19-group-contacts-management.md). + +## Solution + +Alternatives: + +- [Use group connections for sending direct messages](#solution-1-use-group-connections-for-direct-messages). +- [Don't create direct connections during group handshake, allow to create on demand](#solution-2-only-create-direct-connections-on-demand). +- [Only create direct connections during group handshake if group allows direct messages between members](#solution-3-create-direct-connections-only-if-group-setting-allows). + +### Solution 1. Use group connections for direct messages + +Pros: + +- Doesn't require additional negotiation. +- No additional traffic on subscriptions. + +Cons: + +- Breaks chat model - currently group connections and direct connections are treated as different entities on chat business logic level, this would require reworking group connections to allow to function as direct contact connections: + - Sending messages, files, saving chat items. + - Assigning connection to both contact and group member in connections table. Without complex schema migration, deletion of either contact or group member triggers cascade deletion of the connection. Probably no downgrade path. + - Additional field in chat protocol message envelopes to separate group and direct messages. + - Leaving group (and deleting group members) would require keeping connection if contact was used. We do the same thing for profiles currently, but profiles are local only and knowledge about connections would remain on servers; etc. +- Agent abstractions will also leak: + - Integrity errors, skipped errors - can be received "to contact" even if bad/skipped previous messages were sent "to group member" and vice versa. + - On decryption errors it's impossible to know intended entity - contact or group member. + - No connection level isolation between activity as contact and activity as member. +- Unclear contact merging. Connecting with the same person in different groups would merge their contacts, which would lead to using the same connection for different groups. Connection negotiated via host in one group would be used for another group, likely with a different threat model. Even further breaks model and connection level isolation. +- In new planned group protocol (sparsely connected group) hosting members would be relaying messages between unconnected group members, which means not only direct connection negotiation will be carried out via host, but messages intended for contact would be passed via host in plaintext - this would be an unacceptable tradeoff. + +### Solution 2. Only create direct connections on demand + +Pros: + +- Additional traffic on subscriptions only for created contacts. +- Less required changes to chat protocol and model. Connection isolation between member and contact. + +Cons: + +- Negotiation of a new direct connection via group connection involves several steps. In general case it will be delayed by clients availability. During negotiation sending messages should either be prohibited or messages should be created as pending. +- More traffic on subscription than with solution 1. Still probably orders less traffic for contacts than currently in large groups. + +See Group contacts management rfc (link above) for design thoughts in first approximation. Though it's slightly outdated some changes to protocol messages will likely apply currently. Additional protocol messages would be required for sending invitation to a direct connection via group connection, e.g.: + +```haskell +XGrpDirectInv :: ConnReqInvitation -> ChatMsgEvent 'Json +``` + +Both clients should take into account whether they're incognito in group - if yes, share the same profile. + +### Solution 3. Create direct connections only if group setting allows + +Same pros and cons as for solution 2. + +Additional pros: + +- In groups that allow direct messages between members no additional negotiation is required. This can be convenient for small groups of people who know each other, or for connecting groups of devices. On the other hand in such groups it can be easy to establish them on demand without much delay anyway. + +Additional cons: + +- More traffic on subscriptions than with solution 2. Since the biggest decrease in traffic is still expected to come from large groups, this may be a good trade-off. Additionally direct connection creation can be disabled not only based on group setting, but also based on current member count (same as for group receipts). The host would include it in invitation and introduction messages and member clients would know not to create additional connections. + +Group state may be de-synchronized and clients can have different value for "direct messages allowed" setting - there's two options how to process such case: + +- Either don't establish connection if any of group members has it as off; +- Or establish or not based on group member introduction - establish if it has direct connection even if the setting is off, since it means that's what was communicated by host to the introduced member. Optionally host can remove direct connection invitation from introduction if the rule doesn't allow it but invited member still offered it - in case of race with preferences update or incorrect behavior of invited member client. diff --git a/docs/rfcs/2023-08-26-ios-notifications.md b/docs/rfcs/2023-08-26-ios-notifications.md new file mode 100644 index 0000000000..74ee58932b --- /dev/null +++ b/docs/rfcs/2023-08-26-ios-notifications.md @@ -0,0 +1,26 @@ +# Problem + +iOS notifications are not stable, and crash in the background for many users. The reason for that is concurrent database access from the main app and from NSE processes. While there are measures preventing NSE execution while the main app is running, there is nothing preventing continued NSE execution after the app returns to the foreground. + +While iOS creates a new instance of the notification service class for each incoming mutable notification, it re-uses the same process. + +In addition to that, while NSE avoids subscribing to the existing connections to avoid disrupting the subscriptions from the main app, it can create new connections, specifically for legacy file transfers (not important) and for member connections (very important), so the NSE may continue receiving the messages from these new connections, creating database contention, and at the same time the main app won't subscribe to these connections until it is restarted. + +# Possible solutions + +We could consider using WAL mode to increase SQLite concurrency, but it means that WAL files have to be included in the database archive, which is currently not done. It is unclear if it can only be one or multiple files. + +We should also consider some mechanism of suspending NSE when the app returns to the foreground, e.g. via: +- checking the shared preference in message reception loop, although it may be too late to prevent the database contention. +- checking the shared preference frequently and suspending the core, but it can be expensive. +- using Mach messages to communicate to NSE that app is resumed so it can suspend itself. + +The problem with connection subscriptions can be addressed in this way: + +1. Any connections created in NSE will use additional parameter in all APIs to avoid creating subscriptions. +2. The information about these connections will be stored in the database as "requiring subscription". +3. The main app will subscribe to them when it is resumed. + +This is similar to how XFTP files are marked as "to be received" when the message is received by NSE. + +If this is agreed, then this require changing the whole stack, including SMP protocol, as currently the subscription is done automatically when the connection is created. diff --git a/docs/rfcs/2023-08-28-groups-improvements.md b/docs/rfcs/2023-08-28-groups-improvements.md new file mode 100644 index 0000000000..7a653fcb26 --- /dev/null +++ b/docs/rfcs/2023-08-28-groups-improvements.md @@ -0,0 +1,112 @@ +# Groups improvements + +See also: +- [Group contacts management](./2022-10-19-group-contacts-management.md). +- [Create groups without establishing direct connections](./2023-08-10-groups-wt-contacts.md). + +## Problem + +Establishing connections in groups is unstable and uses a lot of traffic. There are several areas for improvement that that could help optimize it: + +- Joining group member prematurely creates direct and group connections for each member. + + Some members may never come online, and that traffic would be completely wasted. + + Instead of creating direct connections, we could allow to send direct messages inside group, and optionally have a separate protocol for automating establishing direct connection with member via them. + +- Host sends N introduction messages (XGrpMemIntro) to joining member. Instead they could be batched. + +## Possible solutions + +### Improved group handshake protocol + +Below are proposed changes to group handshake protocol to reduce traffic and improve stability. + +Each joining member creates a new temporary per group address for introduced members to connect via. Joining member sends it to host when accepting group invitation. + +``` haskell +XGrpAcptAddress :: MemberId -> ConnReqContact -> ChatMsgEvent 'Json +``` + +Host sends group introductions in batches, batching smaller messages first (introductions of members without profile picture). + +For each received batch of N introductions joining member creates N transient per member identifiers (MemberCodes) and replies to host with batched XGrpMemInv messages including these identifiers. Joining member would then use them to verify contact requests from introduced members. + +How is MemberCode different from MemberId? - MemberId is known to all group members and is constant per member per group. MemberCode would be known only to host and to introduced member (of existing members), so other members wouldn't be able to impersonate one another when requesting connection with joining member. An introduced member can still pass their identifier + joining member address to another member or outside of group, but it is no different to passing currently shared invitation links. + +```haskell +newtype MemberCode = MemberCode {unMemberCode :: ByteString} + +XGrpMemInvCode :: MemberId -> MemberCode -> ChatMsgEvent 'Json + +-- instead of / in addition to batching message could be + +type MemberCodes = Map MemberId MemberCode + +XGrpMemInvCodes :: MemberCodes -> ChatMsgEvent 'Json +``` + +Host includes joining member address and code (unique for each introduced member) into XGrpMemFwd messages instead of invitation links: + +```haskell +XGrpMemFwdCode :: MemberInfo -> ConnReqContact -> MemberCode -> ChatMsgEvent 'Json +``` + +Introduced members send contact requests with a new message XGroupMember / XIntroduced (similar to XInfo or XContact, see `processUserContactRequest`): + +```haskell +XIntroduced :: MemberInfo -> MemberCode -> ChatMsgEvent 'Json +``` + +Joinee verifies profile and code and automatically accepts contact request. They both assign resulting connection to respective group member record, without creating contact. + +After (if) all introduced members have connected, joining member deletes per group address. Possibly it can also be deleted after expiration interval. + +#### Group links + +We can reduce number of steps taken to join group via group link: +- Do not create direct connection and contact with group link host, instead use the connection resulting from contact request as a group connection, and assign it to a group member record. +- Host to not send XGrpInv message, joining member to not wait for it, instead joining member would initiate with XGrpAcptAddress after establishing connection via group link. + +In addition to their profile, host includes MemberId of joining member into confirmation when accepting group link join request, using new message: + +```haskell +XGroupLinkInfo :: Profile -> MemberId -> ChatMsgEvent 'Json +``` + +Joining member initially doesn't know group profile, they create a placeholder group with a new dummy profile (alternatively, we could include at least group display name into group link). After connection is established, host sends XGrpInfo containing group profile to joining member. This can happen in parallel with group handshake started by XGrpAcptAddress. + +Group profile could also be included into XGroupLinkInfo if not for the limitation on size if both host's profile and group profile contain pictures. + +![Adding member to the group](./diagrams/2023-08-28-groups-improvements.svg) + +#### Clients compatibility + +We have a [proposed mechanism](https://github.com/simplex-chat/simplex-chat/pull/2886) for communicating "chat protocol version" between clients. + +Sending and processing new protocol messages would only be supported by updated clients. + +Trying to support both protocols across different members in the same group would require complex logic: + +Host would have to send introduced members versions, joining member would provide both address or invitation links depending on each members' versions, host would forward accordingly. + +Instead we could assign "chat protocol version" per group and share it with members as part of group profile, and make a two-stage release when members would first be able to update and get new processing logic, but have it disabled until next release. + +After group switching to new processing logic old clients wouldn't be able to connect in groups. + +How should existing groups be switched? +- Owner user action? +- Owner client deciding automatically? +- In case group has multiple owners - which owner(s) can / should decide? +- Prohibited until all / part of existing members don't update? How to request members to update? +- Old clients will not be able to process and save group chat version from group profile update. + +### Sending direct messages inside group + +Group messages are sent by broadcasting them to all group member connections. As a replacement for creating additional direct connections in group we can allow to send message directly to members via group member connections. The UX would be to choose whether to send to group or to a specific member via compose view. + +Possible approach is to extend ExtMsgContent with `direct :: Maybe Bool` field, which would only be considered for group messages. + +Chat items should store information of receiving member database ID (for sending member) and of message being direct (for receiving member). Perhaps it could be a single field `direct_member_id`, which would be the same as `group_member_id` for received messages. + +TODO - consider whether `connection_id` or `group_id` or both should be assigned in `messages` table. diff --git a/docs/rfcs/2023-09-12-group-member-contacts.md b/docs/rfcs/2023-09-12-group-member-contacts.md new file mode 100644 index 0000000000..0d403b0641 --- /dev/null +++ b/docs/rfcs/2023-09-12-group-member-contacts.md @@ -0,0 +1,56 @@ +# Groups member contacts + +## Problem + +Ability to send direct messages to group members, w/t creating additional direct connections, while keeping existing UX of separate conversations. + +## Solution + +### Protocol + +Same changes on chat protocol level as for direct messages: + +```haskell +data MessageScope = MSGroup | MSDirect + +-- ExtMsgContent extended with scope +data ExtMsgContent = ExtMsgContent + { ... + scope :: Maybe MessageScope + } +``` + +Changes to MsgRef are not necessary - it would be impossible to quote group message directly to member contact. + +### Model + +If member doesn't have existing contact (e.g. legacy, merged or "member contact" created before), button "Send direct message" / "Open direct chat" in UI would create a new "member contact" record in contacts table that would use the same connection as group member. + +New API: + +```haskell +APICreateMemberContact GroupId GroupMemberId +``` + +- would create a new contact record and assign contact_id to group_member record +- should be unmergeable so that the same connection is not used for member across groups + - flag + - or group_member_id in contacts table +- member removal (member leaves or removed) and deletion (group is deleted) should check if "member contact" exists for the same connection, if yes connection should be kept +- contact deletion should also check if member exists +- due to ON DELETE CASCADE constraints entity id should be first set to null before deleting contact/member record + +On receiving group message with MSDirect scope, if "member contact" doesn't exist it should be created (same as above). + +What if member record already had regular contact assigned? (a contact with a separate connection and no group_member_id) It can happen if merge completed for these members previously, and one of the members deleted contact; or if merge has only completed for one of members. + +- Assigning chat items received as "member contact" to a regular contact would mix messages from different connections in the same conversation. This would practically be indistinguishable in UI, but would further complicate understanding of connection level errors. +- Replacing contact_id for group_members record would seem to user as if previous messages were lost when chat is entered via "open direct chat" button, unless there's an indication inside this new chat that an old contact exists separately (requires yet additional field on contact - main_contact_id?). + +When next messages are received read connection entity based on message scope? (parameterize getConnectionEntity, toConnection with message scope). Move message scope on a level above ExtMsgContent? If it's on AChatMsgEvent level - this would allow only MSG agent messages and not make it a fully fledged connection entity. Should "member contact" be able to receive any messages other than XMsgNew and messages referring by shared msg id? If not, maybe it shouldn't be treated as connection entity and instead "member contact" record should be read ad-hoc when processing these messages (for example see processGroupScopeMsg for group direct message). For example, should "member contacts" be available for inviting to other groups? Probably not - if they were, connection established in one group would be reused in another group. + +We could also make distinction between regular contacts and "member contacts" in chat list - e.g. show both group and member avatar/icon, or include group name in conversation name. This would also improve clarity for users which contacts were created for which reason and to better understand consequences to deleting it. + +TODO: + +We should also double check that removed members messages are dropped if received - right now their connections are deleted so it's practically not possible for them to send after being removed; if their connection is kept for "member contact" purpose though, they would still be able to send group messages. diff --git a/docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd b/docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd new file mode 100644 index 0000000000..591c30445e --- /dev/null +++ b/docs/rfcs/diagrams/2023-08-28-groups-improvements.mmd @@ -0,0 +1,40 @@ +sequenceDiagram + participant M as N existing
members + participant A as Alice + participant B as Bob + + note over A, B: 1. send and accept group invitation /
join via group link + alt host invites contact + A ->> B: x.grp.inv
invite Bob to group
(via contact connection) + else user joins via group link + B ->> A: request to join group via link + A ->> B: auto-accept
x.group.link.info with host's profile
and joining member MemberId
establish group member connection + A ->> B: x.grp.info
group profile + end + + note right of B: when joining via group link
Bob doesn't wait for x.grp.info
and initiates group handshake
with x.grp.acpt.address
after establishing connection + + note over B: create per group address + B ->> A: x.grp.acpt.address
accept invitation
and send address to connect
(via member connection) + B ->> A: establish group member connection + + note over M, B: 2. introduce new member Bob to all existing members + A ->> M: x.grp.mem.new
"announce" Bob
to existing members
(via member connections) + + loop batched + A ->> B: x.grp.mem.intro * N
"introduce" members
(via member connection) + note over B: create N MemberCodes + B ->> A: x.grp.mem.inv.code
unique MemberCodes
for all members
(via member connection) + end + + A ->> M: x.grp.mem.fwd.code
forward address
and unique MemberCodes
to all members
(via member connections) + + note over M, B: 3. establish group member connection + M ->> B: request group member connection
x.introduced with MemberCode + B ->> M: verify MemberCode, auto-accept + + note over M, B: no contact deduplication + + opt all introduced members connected / expiration + note over B: delete per group address + end diff --git a/docs/rfcs/diagrams/2023-08-28-groups-improvements.svg b/docs/rfcs/diagrams/2023-08-28-groups-improvements.svg new file mode 100644 index 0000000000..34529d5329 --- /dev/null +++ b/docs/rfcs/diagrams/2023-08-28-groups-improvements.svg @@ -0,0 +1 @@ +BobAliceN existingmembersBobAliceN existingmembers1. send and accept group invitation /join via group linkalt[host invites contact][user joins via group link]when joining via group linkBob doesn't wait for x.grp.infoand initiates group handshakewith x.grp.acpt.addressafter establishing connectioncreate per group address2. introduce new member Bob to all existing memberscreate N MemberCodesloop[batched]3. establish group member connectionno contact deduplicationdelete per group addressopt[all introducedmembersconnected /expiration]x.grp.invinvite Bob to group(via contact connection)request to join group via linkauto-acceptx.group.link.info with host's profileand joining member MemberIdestablish group member connectionx.grp.infogroup profilex.grp.acpt.addressaccept invitationand send address to connect(via member connection)establish group member connectionx.grp.mem.new"announce" Bobto existing members(via member connections)x.grp.mem.intro * N"introduce" members(via member connection)x.grp.mem.inv.codeunique MemberCodesfor all members(via member connection)x.grp.mem.fwd.codeforward addressand unique MemberCodesto all members(via member connections)request group member connectionx.introduced with MemberCodeverify MemberCode, auto-accept \ No newline at end of file diff --git a/package.yaml b/package.yaml index 634a92f6bf..ba87c449a8 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.2.3.0 +version: 5.3.1.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme @@ -10,6 +10,7 @@ copyright: 2020-22 simplex.chat category: Web, System, Services, Cryptography extra-source-files: - README.md + - cabal.project dependencies: - aeson == 2.0.* @@ -91,8 +92,16 @@ executables: - -threaded simplex-broadcast-bot: - source-dirs: apps/simplex-broadcast-bot - main: Main.hs + source-dirs: apps/simplex-broadcast-bot/src + main: ../Main.hs + dependencies: + - simplex-chat + ghc-options: + - -threaded + + simplex-directory-service: + source-dirs: apps/simplex-directory-service/src + main: ../Main.hs dependencies: - simplex-chat ghc-options: @@ -100,7 +109,10 @@ executables: tests: simplex-chat-test: - source-dirs: tests + source-dirs: + - tests + - apps/simplex-broadcast-bot/src + - apps/simplex-directory-service/src main: Test.hs dependencies: - simplex-chat diff --git a/scripts/desktop/build-desktop-mac-ci.sh b/scripts/desktop/build-desktop-mac-ci.sh new file mode 100755 index 0000000000..07a3db9c8e --- /dev/null +++ b/scripts/desktop/build-desktop-mac-ci.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -e + +trap "rm apps/multiplatform/local.properties || true; rm local.properties || true; rm /tmp/simplex.keychain || true" EXIT +echo "desktop.mac.signing.identity=Developer ID Application: SimpleX Chat Ltd (5NN7GUYB6T)" >> apps/multiplatform/local.properties +echo "desktop.mac.signing.keychain=/tmp/simplex.keychain" >> apps/multiplatform/local.properties +echo "desktop.mac.notarization.apple_id=$APPLE_SIMPLEX_NOTARIZATION_APPLE_ID" >> apps/multiplatform/local.properties +echo "desktop.mac.notarization.password=$APPLE_SIMPLEX_NOTARIZATION_PASSWORD" >> apps/multiplatform/local.properties +echo "desktop.mac.notarization.team_id=5NN7GUYB6T" >> apps/multiplatform/local.properties +echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode - > /tmp/simplex.keychain + +scripts/desktop/build-lib-mac.sh +cd apps/multiplatform +./gradlew packageDmg +./gradlew notarizeDmg diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh new file mode 100755 index 0000000000..f69b897736 --- /dev/null +++ b/scripts/desktop/build-lib-linux.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +OS=linux +ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`} +GHC_VERSION=8.10.7 + +BUILD_DIR=dist-newstyle/build/$ARCH-$OS/ghc-${GHC_VERSION}/simplex-chat-* + +rm -rf $BUILD_DIR +cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN' --ghc-options="-optl-L$(ghc --print-libdir)/rts -optl-Wl,--as-needed,-lHSrts_thr-ghc$GHC_VERSION" +cd $BUILD_DIR/build +#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so +#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so +mkdir deps +ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ + +cd - + +rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ +rm -rf apps/multiplatform/desktop/build/cmake + +mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +scripts/desktop/prepare-vlc-linux.sh diff --git a/scripts/desktop/build-lib-mac.sh b/scripts/desktop/build-lib-mac.sh new file mode 100755 index 0000000000..b5d738be98 --- /dev/null +++ b/scripts/desktop/build-lib-mac.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +OS=mac +ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" +if [ "$ARCH" == "arm64" ]; then + ARCH=aarch64 +fi +LIB_EXT=dylib +LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT +GHC_LIBS_DIR=$(ghc --print-libdir) + +BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-* + +rm -rf $BUILD_DIR +cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/rts -optl-lHSrts_thr-ghc8.10.7 -optl-lffi" + +cd $BUILD_DIR/build +mkdir deps 2> /dev/null + +# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available +cp $GHC_LIBS_DIR/rts/libffi.dylib ./deps + +DYLIBS=`otool -L $LIB | grep @rpath | tail -n +2 | cut -d' ' -f 1 | cut -d'/' -f2` +RPATHS=`otool -l $LIB | grep "path "| cut -d' ' -f11` + +PROCESSED_LIBS=() + +function copy_deps() { + local LIB=$1 + if [[ "${PROCESSED_LIBS[*]}" =~ "$LIB" ]]; then + return 0 + fi + + PROCESSED_LIBS+=$LIB + local DYLIBS=`otool -L $LIB | grep @rpath | tail -n +2 | cut -d' ' -f 1 | cut -d'/' -f2` + local NON_FINAL_RPATHS=`otool -l $LIB | grep "path "| cut -d' ' -f11` + local RPATHS=`otool -l $LIB | grep "path "| cut -d' ' -f11 | sed "s|@loader_path/..|$GHC_LIBS_DIR|"` + + cp $LIB ./deps + if [[ "$NON_FINAL_RPATHS" == *"@loader_path/.."* ]]; then + # Need to point the lib to @loader_path instead + install_name_tool -add_rpath @loader_path ./deps/`basename $LIB` + fi + #echo LIB $LIB + #echo DYLIBS ${DYLIBS[@]} + #echo RPATHS ${RPATHS[@]} + + for DYLIB in $DYLIBS; do + for RPATH in $RPATHS; do + if [ -f "$RPATH/$DYLIB" ]; then + #echo DEP IS "$RPATH/$DYLIB" + if [ ! -f "deps/$DYLIB" ]; then + cp "$RPATH/$DYLIB" ./deps + fi + copy_deps "$RPATH/$DYLIB" + fi + done + done +} + +copy_deps $LIB +rm deps/`basename $LIB` + +if [ -e deps/libHSdrct-*.$LIB_EXT ]; then + LIBCRYPTO_PATH=$(otool -l deps/libHSdrct-*.$LIB_EXT | grep libcrypto | cut -d' ' -f11) + install_name_tool -change $LIBCRYPTO_PATH @rpath/libcrypto.1.1.$LIB_EXT deps/libHSdrct*.$LIB_EXT + cp $LIBCRYPTO_PATH deps/libcrypto.1.1.$LIB_EXT + chmod 755 deps/libcrypto.1.1.$LIB_EXT +fi + +cd - + +rm -rf apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +rm -rf apps/multiplatform/desktop/src/jvmMain/resources/libs/$OS-$ARCH/ +rm -rf apps/multiplatform/desktop/build/cmake + +mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp -r $BUILD_DIR/build/deps apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/ +scripts/desktop/prepare-vlc-mac.sh diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh new file mode 100755 index 0000000000..35e62481db --- /dev/null +++ b/scripts/desktop/make-appimage-linux.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} + +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +multiplatform_dir=$root_dir/apps/multiplatform +release_app_dir=$root_dir/apps/multiplatform/release/main/app + +cd $multiplatform_dir +libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/deps/libHSdirect-sqlcipher-*.so | grep libcrypto | cut -d'=' -f 2 | cut -d ' ' -f 2) +trap "rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` 2> /dev/null || true" EXIT +cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*/deps + +./gradlew createDistributable +rm common/src/commonMain/cpp/desktop/libs/*/deps/`basename $libcrypto_path` +rm desktop/src/jvmMain/resources/libs/*/`basename $libcrypto_path` + +rm -rf $release_app_dir/AppDir 2>/dev/null +mkdir -p $release_app_dir/AppDir/usr + +cd $release_app_dir/AppDir +cp -r ../*imple*/{bin,lib} usr +cp usr/lib/simplex.png . + +# For https://github.com/TheAssassin/AppImageLauncher to be able to show the icon +mkdir -p usr/share/{icons,metainfo,applications} +cp usr/lib/simplex.png usr/share/icons + +ln -s usr/bin/*imple* AppRun +cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*imple*.desktop chat.simplex.app.desktop +sed -i 's|Exec=.*|Exec=simplex|g' *imple*.desktop +sed -i 's|Icon=.*|Icon=simplex|g' *imple*.desktop +cp *imple*.desktop usr/share/applications/ +cp $multiplatform_dir/desktop/src/jvmMain/resources/distribute/*.appdata.xml usr/share/metainfo + +if [ ! -f ../appimagetool-x86_64.AppImage ]; then + wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage + chmod +x ../appimagetool-x86_64.AppImage +fi +../appimagetool-x86_64.AppImage . + +mv *imple*.AppImage ../../ diff --git a/scripts/desktop/prepare-vlc-linux.sh b/scripts/desktop/prepare-vlc-linux.sh new file mode 100755 index 0000000000..e1cfa7e9fc --- /dev/null +++ b/scripts/desktop/prepare-vlc-linux.sh @@ -0,0 +1,74 @@ +#!/bin/bash + +set -e + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/linux-x86_64/deps/vlc + +mkdir $vlc_dir || exit 0 + + +cd /tmp +mkdir tmp 2>/dev/null || true +cd tmp +curl https://github.com/cmatomic/VLCplayer-AppImage/releases/download/3.0.11.1/VLC_media_player-3.0.11.1-x86_64.AppImage -L -o appimage +chmod +x appimage +./appimage --appimage-extract +cp -r squashfs-root/usr/lib/* $vlc_dir +cd ../ +rm -rf tmp +exit 0 + + +# This is currently unneeded +cd /tmp +( +mkdir tmp +cd tmp +curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlc5_3.0.9.2-1_amd64.deb -o libvlc +ar p libvlc data.tar.xz > data.tar.xz +tar -xvf data.tar.xz +mv usr/lib/x86_64-linux-gnu/libvlc.so{.5,} +cp usr/lib/x86_64-linux-gnu/libvlc.so* $vlc_dir +cd ../ +rm -rf tmp +) + +( +mkdir tmp +cd tmp +curl http://archive.ubuntu.com/ubuntu/pool/universe/v/vlc/libvlccore9_3.0.9.2-1_amd64.deb -o libvlccore +ar p libvlccore data.tar.xz > data.tar.xz +tar -xvf data.tar.xz +cp usr/lib/x86_64-linux-gnu/libvlccore.so* $vlc_dir +cd ../ +rm -rf tmp +) + +( +mkdir tmp +cd tmp +curl http://mirrors.edge.kernel.org/ubuntu/pool/universe/v/vlc/vlc-plugin-base_3.0.9.2-1_amd64.deb -o plugins +ar p plugins data.tar.xz > data.tar.xz +tar -xvf data.tar.xz +find usr/lib/x86_64-linux-gnu/vlc/plugins/ -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN/../../' {} \; +cp -r usr/lib/x86_64-linux-gnu/vlc/{libvlc*,plugins} $vlc_dir +cd ../ +rm -rf tmp +) + +( +mkdir tmp +cd tmp +curl http://archive.ubuntu.com/ubuntu/pool/main/libi/libidn/libidn11_1.33-2.2ubuntu2_amd64.deb -o idn +ar p idn data.tar.xz > data.tar.xz +tar -xvf data.tar.xz +cp lib/x86_64-linux-gnu/lib* $vlc_dir +cd ../ +rm -rf tmp +) + +find $vlc_dir -maxdepth 1 -name "lib*.so*" -exec patchelf --set-rpath '$ORIGIN' {} \; diff --git a/scripts/desktop/prepare-vlc-mac.sh b/scripts/desktop/prepare-vlc-mac.sh new file mode 100755 index 0000000000..69644bcc16 --- /dev/null +++ b/scripts/desktop/prepare-vlc-mac.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -e + +ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}" +if [ "$ARCH" == "arm64" ]; then + ARCH=aarch64 + vlc_arch=arm64 +else + vlc_arch=intel64 +fi +vlc_version=3.0.19 + +function readlink() { + echo "$(cd "$(dirname "$1")"; pwd -P)" +} + +root_dir="$(dirname "$(dirname "$(readlink "$0")")")" +vlc_dir=$root_dir/apps/multiplatform/common/src/commonMain/cpp/desktop/libs/mac-$ARCH/deps/vlc +#rm -rf $vlc_dir +mkdir -p $vlc_dir/vlc || exit 0 + +cd /tmp +mkdir tmp 2>/dev/null || true +cd tmp +curl https://github.com/simplex-chat/vlc/releases/download/v$vlc_version/vlc-macos-$ARCH.zip -L -o vlc +unzip -oqq vlc +install_name_tool -add_rpath "@loader_path/VLC.app/Contents/MacOS/lib" vlc-cache-gen +cd VLC.app/Contents/MacOS/lib +for lib in $(ls *.dylib); do install_name_tool -add_rpath "@loader_path" $lib 2> /dev/null || true; done +cd ../plugins +for lib in $(ls *.dylib); do + install_name_tool -add_rpath "@loader_path/../../" $lib 2> /dev/null || true +done +cd .. +../../../vlc-cache-gen plugins +cp lib/* $vlc_dir/ +cp -r -p plugins/ $vlc_dir/vlc/plugins +cd ../../../../ +rm -rf tmp diff --git a/scripts/ios/export-localizations.sh b/scripts/ios/export-localizations.sh index ee97415bc0..cc6eed25a9 100755 --- a/scripts/ios/export-localizations.sh +++ b/scripts/ios/export-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en cs de es fr it ja nl pl ru zh-Hans ) +langs=( en bg cs de es fi fr it ja nl pl ru uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" diff --git a/scripts/ios/import-localizations.sh b/scripts/ios/import-localizations.sh index 18b009d48b..c699966d79 100755 --- a/scripts/ios/import-localizations.sh +++ b/scripts/ios/import-localizations.sh @@ -2,7 +2,7 @@ set -e -langs=( en cs de es fr it ja nl pl ru zh-Hans ) +langs=( en bg cs de es fi fr it ja nl pl ru th uk zh-Hans ) for lang in "${langs[@]}"; do echo "***" diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 3626f25eb2..2ac9c9f1f3 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."fdbfe0e8d159f394f6eb0f5168620da8694033cd" = "0c0r14kzcp9rmc1nxypknaa93aczl13yxyaj1c0zrd1b17c2m8cx"; + "https://github.com/simplex-chat/simplexmq.git"."53c793d5590d3c781aa3fbf72993eee262c7aa83" = "0f0ldlgqwrapgfw5gnaj00xvb14c8nykyjr9fhy79h4r16g614x8"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 294173de02..3b5bce6dd7 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -1,11 +1,11 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.35.0. +-- This file has been generated from package.yaml by hpack version 0.35.2. -- -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.2.3.0 +version: 5.3.1.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -28,6 +28,7 @@ library Simplex.Chat Simplex.Chat.Archive Simplex.Chat.Bot + Simplex.Chat.Bot.KnownContacts Simplex.Chat.Call Simplex.Chat.Controller Simplex.Chat.Core @@ -105,7 +106,16 @@ library Simplex.Chat.Migrations.M20230618_favorite_chats Simplex.Chat.Migrations.M20230621_chat_item_moderations Simplex.Chat.Migrations.M20230705_delivery_receipts + Simplex.Chat.Migrations.M20230721_group_snd_item_statuses + Simplex.Chat.Migrations.M20230814_indexes + Simplex.Chat.Migrations.M20230827_file_encryption + Simplex.Chat.Migrations.M20230829_connections_chat_vrange + Simplex.Chat.Migrations.M20230903_connections_to_subscribe + Simplex.Chat.Migrations.M20230913_member_contacts + Simplex.Chat.Migrations.M20230914_member_probes Simplex.Chat.Mobile + Simplex.Chat.Mobile.File + Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.WebRTC Simplex.Chat.Options Simplex.Chat.ProfileGenerator @@ -274,12 +284,13 @@ executable simplex-bot-advanced cpp-options: -DswiftJSON executable simplex-broadcast-bot - main-is: Main.hs + main-is: ../Main.hs other-modules: - Options + Broadcast.Bot + Broadcast.Options Paths_simplex_chat hs-source-dirs: - apps/simplex-broadcast-bot + apps/simplex-broadcast-bot/src ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded build-depends: aeson ==2.0.* @@ -374,10 +385,65 @@ executable simplex-chat if flag(swift) cpp-options: -DswiftJSON +executable simplex-directory-service + main-is: ../Main.hs + other-modules: + Directory.Events + Directory.Options + Directory.Service + Directory.Store + Paths_simplex_chat + hs-source-dirs: + apps/simplex-directory-service/src + ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + build-depends: + aeson ==2.0.* + , ansi-terminal >=0.10 && <0.12 + , async ==2.2.* + , attoparsec ==0.14.* + , base >=4.7 && <5 + , base64-bytestring >=1.0 && <1.3 + , bytestring ==0.10.* + , composition ==1.0.* + , constraints >=0.12 && <0.14 + , containers ==0.6.* + , cryptonite >=0.27 && <0.30 + , direct-sqlcipher ==2.3.* + , directory ==1.3.* + , email-validate ==2.3.* + , exceptions ==0.10.* + , filepath ==1.4.* + , http-types ==0.12.* + , memory ==0.15.* + , mtl ==2.2.* + , network >=3.1.2.7 && <3.2 + , optparse-applicative >=0.15 && <0.17 + , process ==1.6.* + , random >=1.1 && <1.3 + , record-hasfield ==1.0.* + , simple-logger ==0.1.* + , simplex-chat + , simplexmq >=5.0 + , socks ==0.6.* + , sqlcipher-simple ==0.4.* + , stm ==2.5.* + , template-haskell ==2.16.* + , terminal ==0.2.* + , text ==1.2.* + , time ==1.9.* + , unliftio ==0.2.* + , unliftio-core ==0.2.* + , zip ==1.7.* + default-language: Haskell2010 + if flag(swift) + cpp-options: -DswiftJSON + test-suite simplex-chat-test type: exitcode-stdio-1.0 main-is: Test.hs other-modules: + Bots.BroadcastTests + Bots.DirectoryTests ChatClient ChatTests ChatTests.Direct @@ -391,9 +457,17 @@ test-suite simplex-chat-test SchemaDump ViewTests WebRTCTests + Broadcast.Bot + Broadcast.Options + Directory.Events + Directory.Options + Directory.Service + Directory.Store Paths_simplex_chat hs-source-dirs: tests + apps/simplex-broadcast-bot/src + apps/simplex-directory-service/src ghc-options: -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded build-depends: aeson ==2.0.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index d5770065ef..b331faf5df 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -35,7 +35,7 @@ import Data.Either (fromRight, rights) import Data.Fixed (div') import Data.Functor (($>)) import Data.Int (Int64) -import Data.List (find, isSuffixOf, partition, sortOn) +import Data.List (find, foldl', isSuffixOf, partition, sortOn) import Data.List.NonEmpty (NonEmpty, nonEmpty) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -48,7 +48,7 @@ import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) import Data.Time.Clock.System (SystemTime, systemToUTCTime) import Data.Word (Word32) -import qualified Database.SQLite.Simple as DB +import qualified Database.SQLite.Simple as SQL import Simplex.Chat.Archive import Simplex.Chat.Call import Simplex.Chat.Controller @@ -69,27 +69,33 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Util +import Simplex.Chat.Util (encryptFile) import Simplex.FileTransfer.Client.Main (maxFileSize) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Description (ValidFileDescription, gb, kb, mb) import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), temporaryAgentError) +import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, temporaryAgentError) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, SQLiteStore (dbNew), execSQL, upMigration, withConnection) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), UserProtocol, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import qualified Simplex.Messaging.TMap as TM import Simplex.Messaging.Transport.Client (defaultSocksProxy) import Simplex.Messaging.Util +import Simplex.Messaging.Version import System.Exit (exitFailure, exitSuccess) import System.FilePath (combine, splitExtensions, takeFileName, ()) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, stdout) @@ -109,6 +115,7 @@ defaultChatConfig = { tcpPort = undefined, -- agent does not listen to TCP tbqSize = 1024 }, + chatVRange = supportedChatVRange, confirmMigrations = MCConsole, defaultServers = DefaultAgentServers @@ -161,6 +168,9 @@ maxMsgReactions = 3 fixedImagePreview :: ImageData fixedImagePreview = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAKVJREFUeF7t1kENACEUQ0FQhnVQ9lfGO+xggITQdvbMzArPey+8fa3tAfwAEdABZQspQStgBssEcgAIkSAJkiAJljtEgiRIgmUCSZAESZAESZAEyx0iQRIkwTKBJEiCv5fgvTd1wDmn7QAP4AeIgA4oW0gJWgEzWCZwbQ7gAA7ggLKFOIADOKBMIAeAEAmSIAmSYLlDJEiCJFgmkARJkARJ8N8S/ADTZUewBvnTOQAAAABJRU5ErkJggg==" +smallGroupsRcptsMemLimit :: Int +smallGroupsRcptsMemLimit = 20 + logCfg :: LogConfig logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} @@ -185,12 +195,12 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize notifyQ <- newTBQueueIO tbqSize + subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty currentCalls <- atomically TM.empty filesFolder <- newTVarIO optFilesFolder - incognitoMode <- newTVarIO False chatStoreChanged <- newTVarIO False expireCIThreads <- newTVarIO M.empty expireCIFlags <- newTVarIO M.empty @@ -199,7 +209,7 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen showLiveItems <- newTVarIO False userXFTPFileConfig <- newTVarIO $ xftpFileConfig cfg tempDirectory <- newTVarIO tempDir - pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, incognitoMode, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile} + pure ChatController {activeTo, firstTime, currentUser, smpAgent, agentAsync, chatStore, chatStoreChanged, idsDrg, inputQ, outputQ, notifyQ, subscriptionMode, chatLock, sndFiles, rcvFiles, currentCalls, config, sendNotification, filesFolder, expireCIThreads, expireCIFlags, cleanupManagerAsync, timedItemThreads, showLiveItems, userXFTPFileConfig, tempDirectory, logFilePath = logFile} where configServers :: DefaultAgentServers configServers = @@ -238,6 +248,8 @@ cfgServers = \case startChatController :: forall m. ChatMonad' m => Bool -> Bool -> Bool -> m (Async ()) startChatController subConns enableExpireCIs startXFTPWorkers = do asks smpAgent >>= resumeAgentClient + unless subConns $ + chatWriteVar subscriptionMode SMOnlyCreate users <- fromRight [] <$> runExceptT (withStoreCtx' (Just "startChatController, getUsers") getUsers) restoreCalls s <- asks agentAsync @@ -247,7 +259,7 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do a1 <- async $ race_ notificationSubscriber agentSubscriber a2 <- if subConns - then Just <$> async (subscribeUsers users) + then Just <$> async (subscribeUsers False users) else pure Nothing atomically . writeTVar s $ Just (a1, a2) when startXFTPWorkers $ do @@ -275,14 +287,14 @@ startChatController subConns enableExpireCIs startXFTPWorkers = do startExpireCIThread user setExpireCIFlag user True -subscribeUsers :: forall m. ChatMonad' m => [User] -> m () -subscribeUsers users = do +subscribeUsers :: forall m. ChatMonad' m => Bool -> [User] -> m () +subscribeUsers onlyNeeded users = do let (us, us') = partition activeUser users subscribe us subscribe us' where subscribe :: [User] -> m () - subscribe = mapM_ $ runExceptT . subscribeUserConnections Agent.subscribeConnections + subscribe = mapM_ $ runExceptT . subscribeUserConnections onlyNeeded Agent.subscribeConnections startFilesToReceive :: forall m. ChatMonad' m => [User] -> m () startFilesToReceive users = do @@ -329,7 +341,13 @@ execChatCommand s = do u <- readTVarIO =<< asks currentUser case parseChatCommand s of Left e -> pure $ chatCmdError u e - Right cmd -> either (CRChatCmdError u) id <$> runExceptT (processChatCommand cmd) + Right cmd -> execChatCommand_ u cmd + +execChatCommand' :: ChatMonad' m => ChatCommand -> m ChatResponse +execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd) + +execChatCommand_ :: ChatMonad' m => Maybe User -> ChatCommand -> m ChatResponse +execChatCommand_ u cmd = either (CRChatCmdError u) id <$> runExceptT (processChatCommand cmd) parseChatCommand :: ByteString -> Either String ChatCommand parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace @@ -389,7 +407,7 @@ processChatCommand = \case asks currentUser >>= atomically . (`writeTVar` Just user'') pure $ CRActiveUser user'' SetActiveUser uName viewPwd_ -> do - tryError (withStore (`getUserIdByName` uName)) >>= \case + tryChatError (withStore (`getUserIdByName` uName)) >>= \case Left _ -> throwChatError CEUserUnknown Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_ SetAllContactReceipts onOff -> withUser $ \_ -> withStore' (`updateAllContactReceipts` onOff) >> ok_ @@ -399,6 +417,12 @@ processChatCommand = \case withStore' $ \db -> updateUserContactReceipts db user' settings ok user SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings + APISetUserGroupReceipts userId' settings -> withUser $ \user -> do + user' <- privateGetUser userId' + validateUserPassword user user' Nothing + withStore' $ \db -> updateUserGroupReceipts db user' settings + ok user + SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do user' <- privateGetUser userId' case viewPwdHash user' of @@ -444,14 +468,16 @@ processChatCommand = \case APIActivateChat -> withUser $ \_ -> do restoreCalls withAgent foregroundAgent - withStoreCtx' (Just "APIActivateChat, getUsers") getUsers >>= void . forkIO . startFilesToReceive + users <- withStoreCtx' (Just "APIActivateChat, getUsers") getUsers + void . forkIO $ subscribeUsers True users + void . forkIO $ startFilesToReceive users setAllExpireCIFlags True ok_ APISuspendChat t -> do setAllExpireCIFlags False withAgent (`suspendAgent` t) ok_ - ResubscribeAllConnections -> withStoreCtx' (Just "ResubscribeAllConnections, getUsers") getUsers >>= subscribeUsers >> ok_ + ResubscribeAllConnections -> withStoreCtx' (Just "ResubscribeAllConnections, getUsers") getUsers >>= subscribeUsers False >> ok_ -- has to be called before StartChat SetTempFolder tf -> do createDirectoryIfMissing True tf @@ -464,9 +490,6 @@ processChatCommand = \case APISetXFTPConfig cfg -> do asks userXFTPFileConfig >>= atomically . (`writeTVar` cfg) ok_ - SetIncognito onOff -> do - asks incognitoMode >>= atomically . (`writeTVar` onOff) - ok_ APIExportArchive cfg -> checkChatStopped $ exportArchive cfg >> ok_ ExportArchive -> do ts <- liftIO getCurrentTime @@ -480,6 +503,18 @@ processChatCommand = \case APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg ExecChatStoreSQL query -> CRSQLResult <$> withStore' (`execSQL` query) ExecAgentStoreSQL query -> CRSQLResult <$> withAgent (`execAgentStoreSQL` query) + SlowSQLQueries -> do + ChatController {chatStore, smpAgent} <- ask + chatQueries <- slowQueries chatStore + agentQueries <- slowQueries $ agentClientStore smpAgent + pure CRSlowSQLQueries {chatQueries, agentQueries} + where + slowQueries st = + liftIO $ + map (uncurry SlowSQLQuery . first SQL.fromQuery) + . sortOn (timeAvg . snd) + . M.assocs + <$> withConnection st (readTVarIO . DB.slow) APIGetChats userId withPCC -> withUserId userId $ \user -> CRApiChats user <$> withStoreCtx' (Just "APIGetChats, getChatPreviews") (\db -> getChatPreviews db user withPCC) APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of @@ -496,10 +531,16 @@ processChatCommand = \case chatItems <- withStore $ \db -> getAllChatItems db user pagination search pure $ CRChatItems user chatItems APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do - (aci@(AChatItem _ _ _ ci), versions) <- withStore $ \db -> + (aci@(AChatItem cType dir _ ci), versions) <- withStore $ \db -> (,) <$> getAChatItem db user chatRef itemId <*> liftIO (getChatItemVersions db itemId) let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions - pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions} + memberDeliveryStatuses <- case (cType, dir) of + (SCTGroup, SMDSnd) -> do + withStore' (`getGroupSndStatuses` itemId) >>= \case + [] -> pure Nothing + memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses + _ -> pure Nothing + pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses} APISendMessage (ChatRef cType chatId) live itemTTL (ComposedMessage file_ quotedItemId_ mc) -> withUser $ \user@User {userId} -> withChatLock "sendMessage" $ case cType of CTDirect -> do ct@Contact {contactId, localDisplayName = c, contactUsed} <- withStore $ \db -> getContact db user chatId @@ -529,21 +570,24 @@ processChatCommand = \case SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline SendFileXFTP -> xftpSndFileTransfer user file fileSize 1 $ CGContact ct where - smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - smpSndFileTransfer file fileSize fileInline = do + smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) + smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled + smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do + subMode <- chatReadVar subscriptionMode (agentConnId_, fileConnReq) <- if isJust fileInline then pure (Nothing, Nothing) - else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing) + else bimap Just Just <$> withAgent (\a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode) let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} chSize <- asks $ fileChunkSize . config withStore' $ \db -> do - ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize + ft@FileTransferMeta {fileId} <- createSndDirectFileTransfer db userId ct file fileInvitation agentConnId_ chSize subMode fileStatus <- case fileInline of Just IFMSent -> createSndDirectInlineFT db ct ft $> CIFSSndTransfer 0 1 _ -> pure CIFSSndStored - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP} + let fileSource = Just $ CF.plain file + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP} pure (fileInvitation, ciFile, ft) prepareMsg :: Maybe FileInvitation -> Maybe CITimed -> m (MsgContainer, Maybe (CIQuote 'CTDirect)) prepareMsg fInv_ timed_ = case quotedItemId_ of @@ -574,9 +618,12 @@ processChatCommand = \case (fInv_, ciFile_, ft_) <- unzipMaybe3 <$> setupSndFileTransfer g (length $ filter memberCurrent ms) timed_ <- sndGroupCITimed live gInfo itemTTL (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership - msg@SndMessage {sharedMsgId} <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) + (msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) mapM_ (sendGroupFileInline ms sharedMsgId) ft_ ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live + withStore' $ \db -> + forM_ sentToMembers $ \GroupMember {groupMemberId} -> + createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) setActive $ ActiveG gName @@ -589,15 +636,17 @@ processChatCommand = \case SendFileSMP fileInline -> smpSndFileTransfer file fileSize fileInline SendFileXFTP -> xftpSndFileTransfer user file fileSize n $ CGGroup g where - smpSndFileTransfer :: FilePath -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - smpSndFileTransfer file fileSize fileInline = do + smpSndFileTransfer :: CryptoFile -> Integer -> Maybe InlineFileMode -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) + smpSndFileTransfer (CryptoFile _ (Just _)) _ _ = throwChatError $ CEFileInternal "locally encrypted files can't be sent via SMP" -- can only happen if XFTP is disabled + smpSndFileTransfer (CryptoFile file Nothing) fileSize fileInline = do let fileName = takeFileName file fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq = Nothing, fileInline, fileDescr = Nothing} fileStatus = if fileInline == Just IFMSent then CIFSSndTransfer 0 1 else CIFSSndStored chSize <- asks $ fileChunkSize . config withStore' $ \db -> do ft@FileTransferMeta {fileId} <- createSndGroupFileTransfer db userId gInfo file fileInvitation chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus, fileProtocol = FPSMP} + let fileSource = Just $ CF.plain file + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol = FPSMP} pure (fileInvitation, ciFile, ft) sendGroupFileInline :: [GroupMember] -> SharedMsgId -> FileTransferMeta -> m () sendGroupFileInline ms sharedMsgId ft@FileTransferMeta {fileInline} = @@ -652,17 +701,19 @@ processChatCommand = \case qText = msgContentText qmc qFileName = maybe qText (T.pack . (fileName :: CIFile d -> String)) ciFile_ qTextOrFile = if T.null qText then qFileName else qText - xftpSndFileTransfer :: User -> FilePath -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) - xftpSndFileTransfer user file fileSize n contactOrGroup = do - let fileName = takeFileName file + xftpSndFileTransfer :: User -> CryptoFile -> Integer -> Int -> ContactOrGroup -> m (FileInvitation, CIFile 'MDSnd, FileTransferMeta) + xftpSndFileTransfer user file@(CryptoFile filePath cfArgs) fileSize n contactOrGroup = do + let fileName = takeFileName filePath fileDescr = FileDescr {fileDescrText = "", fileDescrPartNo = 0, fileDescrComplete = False} fInv = xftpFileInvitation fileName fileSize fileDescr - fsFilePath <- toFSFilePath file - aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) fsFilePath (roundedFDCount n) + fsFilePath <- toFSFilePath filePath + let srcFile = CryptoFile fsFilePath cfArgs + aFileId <- withAgent $ \a -> xftpSendFile a (aUserId user) srcFile (roundedFDCount n) -- TODO CRSndFileStart event for XFTP chSize <- asks $ fileChunkSize . config ft@FileTransferMeta {fileId} <- withStore' $ \db -> createSndFileTransferXFTP db user contactOrGroup file fInv (AgentSndFileId aFileId) chSize - let ciFile = CIFile {fileId, fileName, fileSize, filePath = Just file, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} + let fileSource = Just $ CryptoFile filePath cfArgs + ciFile = CIFile {fileId, fileName, fileSize, fileSource, fileStatus = CIFSSndStored, fileProtocol = FPXFTP} case contactOrGroup of CGContact Contact {activeConn} -> withStore' $ \db -> createSndFTDescrXFTP db user Nothing activeConn ft fileDescr CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) @@ -710,7 +761,7 @@ processChatCommand = \case let changed = mc /= oldMC if changed || fromMaybe False itemLive then do - SndMessage {msgId} <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) ci' <- withStore' $ \db -> do currentTs <- liftIO getCurrentTime when changed $ @@ -744,7 +795,7 @@ processChatCommand = \case (CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier - SndMessage {msgId} <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing + (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing delGroupChatItem user gInfo ci msgId Nothing (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete CTContactRequest -> pure $ chatCmdError (Just user) "not supported" @@ -756,7 +807,7 @@ processChatCommand = \case (CIGroupRcv GroupMember {groupMemberId, memberRole, memberId}, Just itemSharedMId) -> do when (groupMemberId /= mId) $ throwChatError CEInvalidChatItemDelete assertUserGroupRole gInfo $ max GRAdmin memberRole - SndMessage {msgId} <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId + (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId delGroupChatItem user gInfo ci msgId (Just membership) (_, _) -> throwChatError CEInvalidChatItemDelete APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> withChatLock "chatItemReaction" $ case cType of @@ -788,7 +839,7 @@ processChatCommand = \case let GroupMember {memberId = itemMemberId} = chatItemMember g ci rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True checkReactionAllowed rs - SndMessage {msgId} <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) + (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) createdAt <- liftIO getCurrentTime reactions <- withStore' $ \db -> do setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt @@ -912,10 +963,9 @@ processChatCommand = \case pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo) CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" - APIAcceptContact connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do + APIAcceptContact incognito connReqId -> withUser $ \_ -> withChatLock "acceptContact" $ do (user, cReq) <- withStore $ \db -> getContactRequest' db connReqId -- [incognito] generate profile to send, create connection with incognito profile - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing ct <- acceptContactRequest user cReq incognitoProfile pure $ CRAcceptingContactRequest user ct @@ -1120,6 +1170,9 @@ processChatCommand = \case incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) connectionStats <- withAgent (`getConnectionServers` contactConnId ct) pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) + APIGroupInfo gId -> withUser $ \user -> do + (g, s) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> liftIO (getGroupSummary db user gId) + pure $ CRGroupInfo user g s APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) @@ -1206,6 +1259,9 @@ processChatCommand = \case SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn}) SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_}) ContactInfo cName -> withContactName cName APIContactInfo + ShowGroupInfo gName -> withUser $ \user -> do + groupId <- withStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIGroupInfo groupId GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo SwitchContact cName -> withContactName cName APISwitchContact SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember @@ -1221,32 +1277,48 @@ processChatCommand = \case EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId ChatHelp section -> pure $ CRChatHelp section Welcome -> withUser $ pure . CRWelcome - APIAddContact userId -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do + APIAddContact userId incognito -> withUserId userId $ \user -> withChatLock "addContact" . procCmd $ do -- [incognito] generate profile for connection - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile + subMode <- chatReadVar subscriptionMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode + conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode toView $ CRNewContactConnection user conn - pure $ CRInvitation user cReq - AddContact -> withUser $ \User {userId} -> - processChatCommand $ APIAddContact userId - APIConnect userId (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do + pure $ CRInvitation user cReq conn + AddContact incognito -> withUser $ \User {userId} -> + processChatCommand $ APIAddContact userId incognito + APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do + conn'_ <- withStore $ \db -> do + conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId + case (pccConnStatus, customUserProfileId, incognito) of + (ConnNew, Nothing, True) -> liftIO $ do + incognitoProfile <- generateRandomProfile + pId <- createIncognitoProfile db user incognitoProfile + Just <$> updatePCCIncognito db user conn (Just pId) + (ConnNew, Just pId, False) -> liftIO $ do + deletePCCIncognitoProfile db user pId + Just <$> updatePCCIncognito db user conn Nothing + _ -> pure Nothing + case conn'_ of + Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' + Nothing -> throwChatError CEConnectionIncognitoChangeProhibited + APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do + subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq . directMessage $ XInfo profileToSend - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend + dm <- directMessage $ XInfo profileToSend + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode + conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode toView $ CRNewContactConnection user conn pure $ CRSentConfirmation user - APIConnect userId (Just (ACR SCMContact cReq)) -> withUserId userId (`connectViaContact` cReq) - APIConnect _ Nothing -> throwChatError CEInvalidConnReq - Connect cReqUri -> withUser $ \User {userId} -> - processChatCommand $ APIConnect userId cReqUri - ConnectSimplex -> withUser $ \user -> + APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq + APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq + Connect incognito cReqUri -> withUser $ \User {userId} -> + processChatCommand $ APIConnect userId incognito cReqUri + ConnectSimplex incognito -> withUser $ \user -> -- [incognito] generate profile to send - connectViaContact user adminContactReq + connectViaContact user incognito adminContactReq DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect APIListContacts userId -> withUserId userId $ \user -> @@ -1254,8 +1326,9 @@ processChatCommand = \case ListContacts -> withUser $ \User {userId} -> processChatCommand $ APIListContacts userId APICreateMyAddress userId -> withUserId userId $ \user -> withChatLock "createMyAddress" . procCmd $ do - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing - withStore $ \db -> createUserContactLink db user connId cReq + subMode <- chatReadVar subscriptionMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing subMode + withStore $ \db -> createUserContactLink db user connId cReq subMode pure $ CRUserContactLinkCreated user cReq CreateMyAddress -> withUser $ \User {userId} -> processChatCommand $ APICreateMyAddress userId @@ -1267,7 +1340,7 @@ processChatCommand = \case let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} r <- updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing let user' = case r of - CRUserProfileUpdated u' _ _ _ _ -> u' + CRUserProfileUpdated u' _ _ _ -> u' _ -> user pure $ CRUserContactLinkDeleted user' DeleteMyAddress -> withUser $ \User {userId} -> @@ -1290,14 +1363,55 @@ processChatCommand = \case pure $ CRUserContactLinkUpdated user contactLink AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> processChatCommand $ APIAddressAutoAccept userId autoAccept_ - AcceptContact cName -> withUser $ \User {userId} -> do + AcceptContact incognito cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand $ APIAcceptContact connReqId + processChatCommand $ APIAcceptContact incognito connReqId RejectContact cName -> withUser $ \User {userId} -> do connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIRejectContact connReqId - SendMessage chatName msg -> sendTextMessage chatName msg False - SendLiveMessage chatName msg -> sendTextMessage chatName msg True + SendMessage (ChatName cType name) msg -> withUser $ \user -> do + let mc = MCText msg + case cType of + CTDirect -> + withStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case + Right ctId -> do + let chatRef = ChatRef CTDirect ctId + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc + Left _ -> + withStore' (\db -> runExceptT $ getActiveMembersByName db user name) >>= \case + Right [(gInfo, member)] -> do + let GroupInfo {localDisplayName = gName} = gInfo + GroupMember {localDisplayName = mName} = member + processChatCommand $ SendMemberContactMessage gName mName msg + Right (suspectedMember : _) -> + throwChatError $ CEContactNotFound name (Just suspectedMember) + _ -> + throwChatError $ CEContactNotFound name Nothing + CTGroup -> do + gId <- withStore $ \db -> getGroupIdByName db user name + let chatRef = ChatRef CTGroup gId + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc + _ -> throwChatError $ CECommandError "not supported" + SendMemberContactMessage gName mName msg -> withUser $ \user -> do + (gId, mId) <- getGroupAndMemberId user gName mName + m <- withStore $ \db -> getGroupMember db user gId mId + let mc = MCText msg + case memberContactId m of + Nothing -> do + gInfo <- withStore $ \db -> getGroupInfo db user gId + toView $ CRNoMemberContactCreating user gInfo m + processChatCommand (APICreateMemberContact gId mId) >>= \case + cr@(CRNewMemberContact _ Contact {contactId} _ _) -> do + toView cr + processChatCommand $ APISendMemberContactInvitation contactId (Just mc) + cr -> pure cr + Just ctId -> do + let chatRef = ChatRef CTDirect ctId + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc + SendLiveMessage chatName msg -> withUser $ \user -> do + chatRef <- getChatRef user chatName + let mc = MCText msg + processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc SendMessageBroadcast msg -> withUser $ \user -> do contacts <- withStore' (`getUserContacts` user) let cts = filter (\ct -> isReady ct && directOrUsed ct) contacts @@ -1349,19 +1463,20 @@ processChatCommand = \case -- TODO for large groups: no need to load all members to determine if contact is a member (group, contact) <- withStore $ \db -> (,) <$> getGroup db user groupId <*> getContact db user contactId assertDirectAllowed user MDSnd contact XGrpInv_ - let Group gInfo@GroupInfo {membership} members = group + let Group gInfo members = group Contact {localDisplayName = cName} = contact assertUserGroupRole gInfo $ max GRAdmin memRole -- [incognito] forbid to invite contact to whom user is connected incognito when (contactConnIncognito contact) $ throwChatError CEContactIncognitoCantInvite -- [incognito] forbid to invite contacts if user joined the group using an incognito profile - when (memberIncognito membership) $ throwChatError CEGroupIncognitoCantInvite + when (incognitoMembership gInfo) $ throwChatError CEGroupIncognitoCantInvite let sendInvitation = sendGrpInvitation user contact gInfo case contactMember contact members of Nothing -> do gVar <- asks idsDrg - (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing - member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq + subMode <- chatReadVar subscriptionMode + (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode + member <- withStore $ \db -> createNewContactMember db gVar user groupId contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole} @@ -1374,11 +1489,17 @@ processChatCommand = \case Nothing -> throwChatError $ CEGroupCantResendInvitation gInfo cName | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do - ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} <- withStore $ \db -> getGroupInvitation db user groupId + (invitation, ct) <- withStore $ \db -> do + inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db user groupId + (inv,) <$> getContactViaMember db user fromMember + let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation + Contact {activeConn = Connection {peerChatVRange}} = ct withChatLock "joinGroup" . procCmd $ do - agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest . directMessage $ XGrpAcpt (memberId (membership :: GroupMember)) + subMode <- chatReadVar subscriptionMode + dm <- directMessage $ XGrpAcpt (memberId (membership :: GroupMember)) + agentConnId <- withAgent $ \a -> joinConnection a (aUserId user) True connRequest dm subMode withStore' $ \db -> do - createMemberConnection db userId fromMember agentConnId + createMemberConnection db userId fromMember agentConnId (fromJVersionRange peerChatVRange) subMode updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted updateCIGroupInvitationStatus user @@ -1411,7 +1532,7 @@ processChatCommand = \case (Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName _ -> do - msg <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole + (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemRole mId memRole ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent gEvent) toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} @@ -1427,7 +1548,7 @@ processChatCommand = \case deleteMemberConnection user m withStore' $ \db -> deleteGroupMember db user m _ -> do - msg <- sendGroupMessage user gInfo members $ XGrpMemDel mId + (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemDel mId ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) deleteMemberConnection user m @@ -1437,7 +1558,7 @@ processChatCommand = \case APILeaveGroup groupId -> withUser $ \user@User {userId} -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db user groupId withChatLock "leaveGroup" . procCmd $ do - msg <- sendGroupMessage user gInfo members XGrpLeave + (msg, _) <- sendGroupMessage user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) -- TODO delete direct connections that were unused @@ -1468,8 +1589,11 @@ processChatCommand = \case ListMembers gName -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIListMembers groupId - ListGroups -> withUser $ \user -> - CRGroupsList user <$> withStore' (`getUserGroupDetails` user) + APIListGroups userId contactId_ search_ -> withUserId userId $ \user -> + CRGroupsList user <$> withStore' (\db -> getUserGroupsWithSummary db user contactId_ search_) + ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do + ct_ <- forM cName_ $ \cName -> withStore $ \db -> getContactByName db user cName + processChatCommand $ APIListGroups userId (contactId' <$> ct_) search_ APIUpdateGroupProfile groupId p' -> withUser $ \user -> do g <- withStore $ \db -> getGroup db user groupId runUpdateGroupProfile user g p' @@ -1479,14 +1603,17 @@ processChatCommand = \case CRGroupProfile user <$> withStore (\db -> getGroupInfoByName db user gName) UpdateGroupDescription gName description -> updateGroupProfileByName gName $ \p -> p {description} + ShowGroupDescription gName -> withUser $ \user -> + CRGroupDescription user <$> withStore (\db -> getGroupInfoByName db user gName) APICreateGroupLink groupId mRole -> withUser $ \user -> withChatLock "createGroupLink" $ do gInfo <- withStore $ \db -> getGroupInfo db user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole groupLinkId <- GroupLinkId <$> drgRandomBytes 16 + subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId - (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact $ Just crClientData - withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) subMode + withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo cReq mRole APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withChatLock "groupLinkMemberRole " $ do gInfo <- withStore $ \db -> getGroupInfo db user groupId @@ -1503,6 +1630,34 @@ processChatCommand = \case gInfo <- withStore $ \db -> getGroupInfo db user groupId (_, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo pure $ CRGroupLink user gInfo groupLink mRole + APICreateMemberContact gId gMemberId -> withUser $ \user -> do + (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db user gId <*> getGroupMember db user gId gMemberId + assertUserGroupRole g GRAuthor + unless (groupFeatureAllowed SGFDirectMessages g) $ throwChatError $ CECommandError "direct messages not allowed" + case memberConn m of + Just mConn@Connection {peerChatVRange} -> do + unless (isCompatibleRange (fromJVersionRange peerChatVRange) xGrpDirectInvVRange) $ throwChatError CEPeerChatVRangeIncompatible + when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" + subMode <- chatReadVar subscriptionMode + (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing subMode + -- [incognito] reuse membership incognito profile + ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode + pure $ CRNewMemberContact user ct g m + _ -> throwChatError CEGroupMemberNotActive + APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do + (g, m, ct, cReq) <- withStore $ \db -> getMemberContact db user contactId + when (contactGrpInvSent ct) $ throwChatError $ CECommandError "x.grp.direct.inv already sent" + case memberConn m of + Just mConn -> do + let msg = XGrpDirectInv cReq msgContent_ + (sndMsg, _) <- sendDirectMessage mConn msg (GroupId $ groupId (g :: GroupInfo)) + withStore' $ \db -> setContactGrpInvSent db ct True + let ct' = ct {contactGrpInvSent = True} + forM_ msgContent_ $ \mc -> do + ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc) + toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct') ci) + pure $ CRNewMemberContactSentInv user ct' g m + _ -> throwChatError CEGroupMemberNotActive CreateGroupLink gName mRole -> withUser $ \user -> do groupId <- withStore $ \db -> getGroupIdByName db user gName processChatCommand $ APICreateGroupLink groupId mRole @@ -1554,25 +1709,32 @@ processChatCommand = \case asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_ SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCFile "") + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just $ CF.plain f) Nothing (MCFile "") SendImage chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName filePath <- toFSFilePath f - unless (any ((`isSuffixOf` map toLower f)) imageExtensions) $ throwChatError CEFileImageType {filePath} + unless (any (`isSuffixOf` map toLower f) imageExtensions) $ throwChatError CEFileImageType {filePath} fileSize <- getFileSize filePath unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} -- TODO include file description for preview - processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just f) Nothing (MCImage "" fixedImagePreview) + processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage (Just $ CF.plain f) Nothing (MCImage "" fixedImagePreview) ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" - ReceiveFile fileId rcvInline_ filePath_ -> withUser $ \_ -> + ReceiveFile fileId encrypted rcvInline_ filePath_ -> withUser $ \_ -> withChatLock "receiveFile" . procCmd $ do - (user, ft) <- withStore $ \db -> getRcvFileTransferById db fileId - receiveFile' user ft rcvInline_ filePath_ - SetFileToReceive fileId -> withUser $ \_ -> do + (user, ft) <- withStore (`getRcvFileTransferById` fileId) + ft' <- if encrypted then encryptLocalFile ft else pure ft + receiveFile' user ft' rcvInline_ filePath_ + where + encryptLocalFile ft = do + cfArgs <- liftIO $ CF.randomArgs + withStore' $ \db -> setFileCryptoArgs db fileId cfArgs + pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} + SetFileToReceive fileId encrypted -> withUser $ \_ -> do withChatLock "setFileToReceive" . procCmd $ do - withStore' (`setRcvFileToReceive` fileId) + cfArgs <- if encrypted then Just <$> liftIO CF.randomArgs else pure Nothing + withStore' $ \db -> setRcvFileToReceive db fileId cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> withChatLock "cancelFile" . procCmd $ @@ -1663,7 +1825,7 @@ processChatCommand = \case QuitChat -> liftIO exitSuccess ShowVersion -> do let versionInfo = coreVersionInfo $(simplexmqCommitQ) - chatMigrations <- map upMigration <$> withStore' Migrations.getCurrent + chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn) agentMigrations <- withAgent getAgentMigrations pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} DebugLocks -> do @@ -1675,6 +1837,20 @@ processChatCommand = \case stat (AgentStatsKey {host, clientTs, cmd, res}, count) = map B.unpack [host, clientTs, cmd, res, bshow count] ResetAgentStats -> withAgent resetAgentStats >> ok_ + GetAgentSubs -> summary <$> withAgent getAgentSubscriptions + where + summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} = + CRAgentSubs + { activeSubs = foldl' countSubs M.empty activeSubscriptions, + pendingSubs = foldl' countSubs M.empty pendingSubscriptions, + removedSubs = foldl' accSubErrors M.empty removedSubscriptions + } + where + countSubs m SubInfo {server} = M.alter (Just . maybe 1 (+ 1)) server m + accSubErrors m = \case + SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m + _ -> m + GetAgentSubsDetails -> CRAgentSubsDetails <$> withAgent getAgentSubscriptions where withChatLock name action = asks chatLock >>= \l -> withLock l name action -- below code would make command responses asynchronous where they can be slow @@ -1736,43 +1912,41 @@ processChatCommand = \case CTDirect -> withStore $ \db -> getDirectChatItemIdByText' db user cId msg CTGroup -> withStore $ \db -> getGroupChatItemIdByText' db user cId msg _ -> throwChatError $ CECommandError "not supported" - connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse - connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do + connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> m ChatResponse + connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case (Just contact, _) -> pure $ CRContactAlreadyExists user contact (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> drgRandomBytes 16 xContactId <- maybe randomXContactId pure xContactId_ + subMode <- chatReadVar subscriptionMode -- [incognito] generate profile to send - -- if user makes a contact request using main profile, then turns on incognito mode and repeats the request, - -- an incognito profile will be sent even though the address holder will have user's main profile received as well; - -- we ignore this edge case as we already allow profile updates on repeat contact requests; - -- alternatively we can re-send the main profile even if incognito mode is enabled - incognito <- readTVarIO =<< asks incognitoMode incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing let profileToSend = userProfileToSend user incognitoProfile Nothing - connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend $ Just xContactId) + dm <- directMessage (XContact profileToSend $ Just xContactId) + connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq dm subMode let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli - conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId + conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode toView $ CRNewContactConnection user conn pure $ CRSentInvitation user incognitoProfile contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> cId == Just contactId && s /= GSMemRemoved && s /= GSMemLeft - checkSndFile :: MsgContent -> FilePath -> Integer -> m (Integer, SendFileMode) - checkSndFile mc f n = do + checkSndFile :: MsgContent -> CryptoFile -> Integer -> m (Integer, SendFileMode) + checkSndFile mc (CryptoFile f cfArgs) n = do fsFilePath <- toFSFilePath f unlessM (doesFileExist fsFilePath) . throwChatError $ CEFileNotFound f ChatConfig {fileChunkSize, inlineFiles} <- asks config xftpCfg <- readTVarIO =<< asks userXFTPFileConfig - fileSize <- getFileSize fsFilePath + fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cfArgs when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f let chunks = - ((- fileSize) `div` fileChunkSize) fileInline = inlineFileMode mc inlineFiles chunks n fileMode = case xftpCfg of Just cfg + | isJust cfArgs -> SendFileXFTP | fileInline == Just IFMSent || fileSize < minFileSize cfg || n <= 0 -> SendFileSMP fileInline | otherwise -> SendFileXFTP _ -> SendFileSMP fileInline @@ -1796,17 +1970,23 @@ processChatCommand = \case asks currentUser >>= atomically . (`writeTVar` Just user') withChatLock "updateProfile" . procCmd $ do ChatConfig {logLevel} <- asks config - (successes, failures) <- foldM (processAndCount user' logLevel) (0, 0) contacts - pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' successes failures + summary <- foldM (processAndCount user' logLevel) (UserProfileUpdateSummary 0 0 0 []) contacts + pure $ CRUserProfileUpdated user' (fromLocalProfile p) p' summary where - processAndCount user' ll (s, f) ct = (processContact user' ct $> (s + 1, f)) `catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> (s, f + 1) - processContact user' ct = do + processAndCount user' ll s@UserProfileUpdateSummary {notChanged, updateSuccesses, updateFailures, changedContacts = cts} ct = do let mergedProfile = userProfileToSend user Nothing $ Just ct ct' = updateMergedPreferences user' ct mergedProfile' = userProfileToSend user' Nothing $ Just ct' - when (mergedProfile' /= mergedProfile) $ do - void $ sendDirectContactMessage ct' (XInfo mergedProfile') - when (directOrUsed ct') $ createSndFeatureItems user' ct ct' + if mergedProfile' == mergedProfile + then pure s {notChanged = notChanged + 1} + else + let cts' = if mergedPreferences ct == mergedPreferences ct' then cts else ct' : cts + in (notifyContact mergedProfile' ct' $> s {updateSuccesses = updateSuccesses + 1, changedContacts = cts'}) + `catchChatError` \e -> when (ll <= CLLInfo) (toView $ CRChatError (Just user) e) $> s {updateFailures = updateFailures + 1, changedContacts = cts'} + where + notifyContact mergedProfile' ct' = do + void $ sendDirectContactMessage ct' (XInfo mergedProfile') + when (directOrUsed ct') $ createSndFeatureItems user' ct ct' updateContactPrefs :: User -> Contact -> Preferences -> m ChatResponse updateContactPrefs user@User {userId} ct@Contact {activeConn = Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' | contactUserPrefs == contactUserPrefs' = pure $ CRContactPrefsUpdated user ct ct @@ -1825,7 +2005,7 @@ processChatCommand = \case runUpdateGroupProfile user (Group g@GroupInfo {groupProfile = p} ms) p' = do assertUserGroupRole g GROwner g' <- withStore $ \db -> updateGroupProfile db user g p' - msg <- sendGroupMessage user g' ms (XGrpInfo p') + (msg, _) <- sendGroupMessage user g' ms (XGrpInfo p') let cd = CDGroupSnd g' unless (sameGroupProfileInfo p p') $ do ci <- saveSndChatItem user cd msg (CISndGroupEvent $ SGEGroupUpdated p') @@ -1902,10 +2082,6 @@ processChatCommand = \case ci <- saveSndChatItem user (CDDirectSnd ct) msg content toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) setActive $ ActiveG localDisplayName - sendTextMessage chatName msg live = withUser $ \user -> do - chatRef <- getChatRef user chatName - let mc = MCText msg - processChatCommand . APISendMessage chatRef live Nothing $ ComposedMessage Nothing Nothing mc sndContactCITimed :: Bool -> Contact -> Maybe Int -> m (Maybe CITimed) sndContactCITimed live = sndCITimed_ live . contactTimedTTL sndGroupCITimed :: Bool -> GroupInfo -> Maybe Int -> m (Maybe CITimed) @@ -1921,7 +2097,7 @@ processChatCommand = \case drgRandomBytes n = asks idsDrg >>= liftIO . (`randomBytes` n) privateGetUser :: UserId -> m User privateGetUser userId = - tryError (withStore (`getUser` userId)) >>= \case + tryChatError (withStore (`getUser` userId)) >>= \case Left _ -> throwChatError CEUserUnknown Right user -> pure user validateUserPassword :: User -> User -> Maybe UserPwd -> m () @@ -2129,18 +2305,20 @@ receiveFile' user ft rcvInline_ filePath_ = do e -> throwError e acceptFileReceive :: forall m. ChatMonad m => User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> m AChatItem -acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId} rcvInline_ filePath_ = do +acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do unless (fileStatus == RFSNew) $ case fileStatus of RFSCancelled _ -> throwChatError $ CEFileCancelled fName _ -> throwChatError $ CEFileAlreadyReceiving fName case (xftpRcvFile, fileConnReq) of -- direct file protocol (Nothing, Just connReq) -> do - connIds <- joinAgentConnectionAsync user True connReq . directMessage $ XFileAcpt fName + subMode <- chatReadVar subscriptionMode + dm <- directMessage $ XFileAcpt fName + connIds <- joinAgentConnectionAsync user True connReq dm subMode filePath <- getRcvFilePath fileId filePath_ fName True - withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath + withStoreCtx (Just "acceptFileReceive, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnJoined filePath subMode -- XFTP - (Just _xftpRcvFile, _) -> do + (Just XFTPRcvFile {}, _) -> do filePath <- getRcvFilePath fileId filePath_ fName False (ci, rfd) <- withStoreCtx (Just "acceptFileReceive, xftpAcceptRcvFT ...") $ \db -> do -- marking file as accepted and reading description in the same transaction @@ -2148,7 +2326,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI ci <- xftpAcceptRcvFT db user fileId filePath rfd <- getRcvFileDescrByFileId db fileId pure (ci, rfd) - receiveViaCompleteFD user fileId rfd + receiveViaCompleteFD user fileId rfd cryptoArgs pure ci -- group & direct file protocol _ -> do @@ -2179,8 +2357,9 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName | otherwise -> do -- accepting via a new connection - connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation - withStoreCtx (Just "acceptFile, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath + subMode <- chatReadVar subscriptionMode + connIds <- createAgentConnectionAsync user cmdFunction True SCMInvitation subMode + withStoreCtx (Just "acceptFile, acceptRcvFileTransfer") $ \db -> acceptRcvFileTransfer db user fileId connIds ConnNew filePath subMode receiveInline :: m Bool receiveInline = do ChatConfig {fileChunkSize, inlineFiles = InlineFilesConfig {receiveChunks, offerChunks}} <- asks config @@ -2191,11 +2370,11 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) ) -receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> m () -receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} = +receiveViaCompleteFD :: ChatMonad m => User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> m () +receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = when fileDescrComplete $ do rd <- parseFileDescription fileDescrText - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs startReceivingFile user fileId withStoreCtx' (Just "receiveViaCompleteFD, updateRcvFileAgentId") $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) @@ -2213,7 +2392,7 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of asks filesFolder >>= readTVarIO >>= \case Nothing -> do dir <- (`combine` "Downloads") <$> getHomeDirectory - ifM (doesDirectoryExist dir) (pure dir) getTemporaryDirectory + ifM (doesDirectoryExist dir) (pure dir) getChatTempDirectory >>= (`uniqueCombine` fn) >>= createEmptyFile Just filesFolder -> @@ -2241,27 +2420,34 @@ getRcvFilePath fileId fPath_ fn keepHandle = case fPath_ of pure fPath getTmpHandle :: FilePath -> m Handle getTmpHandle fPath = openFile fPath AppendMode `catchThrow` (ChatError . CEFileInternal . show) - uniqueCombine :: FilePath -> String -> m FilePath - uniqueCombine filePath fileName = tryCombine (0 :: Int) - where - tryCombine n = - let (name, ext) = splitExtensions fileName - suffix = if n == 0 then "" else "_" <> show n - f = filePath `combine` (name <> suffix <> ext) - in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) + +uniqueCombine :: MonadIO m => FilePath -> String -> m FilePath +uniqueCombine filePath fileName = tryCombine (0 :: Int) + where + tryCombine n = + let (name, ext) = splitExtensions fileName + suffix = if n == 0 then "" else "_" <> show n + f = filePath `combine` (name <> suffix <> ext) + in ifM (doesFileExist f) (tryCombine $ n + 1) (pure f) + +getChatTempDirectory :: ChatMonad m => m FilePath +getChatTempDirectory = chatReadVar tempDirectory >>= maybe getTemporaryDirectory pure acceptContactRequest :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact -acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do +acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = cp, userContactLinkId, xContactId} incognitoProfile = do + subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile - acId <- withAgent $ \a -> acceptContact a True invId . directMessage $ XInfo profileToSend - withStore' $ \db -> createAcceptedContact db user acId cName profileId cp userContactLinkId xContactId incognitoProfile + dm <- directMessage $ XInfo profileToSend + acId <- withAgent $ \a -> acceptContact a True invId dm subMode + withStore' $ \db -> createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId cp userContactLinkId xContactId incognitoProfile subMode acceptContactRequestAsync :: ChatMonad m => User -> UserContactRequest -> Maybe IncognitoProfile -> m Contact -acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do +acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile = do + subMode <- chatReadVar subscriptionMode let profileToSend = profileToSendOnAccept user incognitoProfile - (cmdId, acId) <- agentAcceptContactAsync user True invId $ XInfo profileToSend + (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode withStore' $ \db -> do - ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId cName profileId p userContactLinkId xContactId incognitoProfile + ct@Contact {activeConn = Connection {connId}} <- createAcceptedContact db user acId (fromJVersionRange cReqChatVRange) cName profileId p userContactLinkId xContactId incognitoProfile subMode setCommandConnId db user cmdId connId pure ct @@ -2308,18 +2494,28 @@ agentSubscriber = do type AgentBatchSubscribe m = AgentClient -> [ConnId] -> ExceptT AgentErrorType m (Map ConnId (Either AgentErrorType ())) -subscribeUserConnections :: forall m. ChatMonad m => AgentBatchSubscribe m -> User -> m () -subscribeUserConnections agentBatchSubscribe user@User {userId} = do +subscribeUserConnections :: forall m. ChatMonad m => Bool -> AgentBatchSubscribe m -> User -> m () +subscribeUserConnections onlyNeeded agentBatchSubscribe user@User {userId} = do -- get user connections ce <- asks $ subscriptionEvents . config - (ctConns, cts) <- getContactConns - (ucConns, ucs) <- getUserContactLinkConns - (gs, mConns, ms) <- getGroupMemberConns - (sftConns, sfts) <- getSndFileTransferConns - (rftConns, rfts) <- getRcvFileTransferConns - (pcConns, pcs) <- getPendingContactConns + (conns, cts, ucs, gs, ms, sfts, rfts, pcs) <- + if onlyNeeded + then do + (conns, entities) <- withStore' getConnectionsToSubscribe + let (cts, ucs, ms, sfts, rfts, pcs) = foldl' addEntity (M.empty, M.empty, M.empty, M.empty, M.empty, M.empty) entities + pure (conns, cts, ucs, [], ms, sfts, rfts, pcs) + else do + withStore' unsetConnectionToSubscribe + (ctConns, cts) <- getContactConns + (ucConns, ucs) <- getUserContactLinkConns + (gs, mConns, ms) <- getGroupMemberConns + (sftConns, sfts) <- getSndFileTransferConns + (rftConns, rfts) <- getRcvFileTransferConns + (pcConns, pcs) <- getPendingContactConns + let conns = concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns] + pure (conns, cts, ucs, gs, ms, sfts, rfts, pcs) -- subscribe using batched commands - rs <- withAgent (`agentBatchSubscribe` concat [ctConns, ucConns, mConns, sftConns, rftConns, pcConns]) + rs <- withAgent $ \a -> agentBatchSubscribe a conns -- send connection events to view contactSubsToView rs cts ce contactLinkSubsToView rs ucs @@ -2328,6 +2524,29 @@ subscribeUserConnections agentBatchSubscribe user@User {userId} = do rcvFileSubsToView rs rfts pendingConnSubsToView rs pcs where + addEntity (cts, ucs, ms, sfts, rfts, pcs) = \case + RcvDirectMsgConnection c (Just ct) -> let cts' = addConn c ct cts in (cts', ucs, ms, sfts, rfts, pcs) + RcvDirectMsgConnection c Nothing -> let pcs' = addConn c (toPCC c) pcs in (cts, ucs, ms, sfts, rfts, pcs') + RcvGroupMsgConnection c _g m -> let ms' = addConn c m ms in (cts, ucs, ms', sfts, rfts, pcs) + SndFileConnection c sft -> let sfts' = addConn c sft sfts in (cts, ucs, ms, sfts', rfts, pcs) + RcvFileConnection c rft -> let rfts' = addConn c rft rfts in (cts, ucs, ms, sfts, rfts', pcs) + UserContactConnection c uc -> let ucs' = addConn c uc ucs in (cts, ucs', ms, sfts, rfts, pcs) + addConn :: Connection -> a -> Map ConnId a -> Map ConnId a + addConn = M.insert . aConnId + toPCC Connection {connId, agentConnId, connStatus, viaUserContactLink, groupLinkId, customUserProfileId, localAlias, createdAt} = + PendingContactConnection + { pccConnId = connId, + pccAgentConnId = agentConnId, + pccConnStatus = connStatus, + viaContactUri = False, + viaUserContactLink, + groupLinkId, + customUserProfileId, + connReqInv = Nothing, + localAlias, + createdAt, + updatedAt = createdAt + } getContactConns :: m ([ConnId], Map ConnId Contact) getContactConns = do cts <- withStore_ ("subscribeUserConnections " <> show userId <> ", getUserContacts") getUserContacts @@ -2516,7 +2735,7 @@ expireChatItems user@User {userId} ttl sync = do createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs contacts <- withStoreCtx' (Just "expireChatItems, getUserContacts") (`getUserContacts` user) loop contacts $ processContact expirationDate - groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (`getUserGroupDetails` user) + groups <- withStoreCtx' (Just "expireChatItems, getUserGroupDetails") (\db -> getUserGroupDetails db user Nothing Nothing) loop groups $ processGroup expirationDate createdAtCutoff where loop :: [a] -> (a -> m ()) -> m () @@ -2701,7 +2920,7 @@ processAgentMsgRcvFile _corrId aFileId msg = liftIO $ updateFileCancelled db user fileId CIFSRcvError getChatItemByFileId db user fileId agentXFTPDeleteRcvFile aFileId fileId - toView $ CRRcvFileError user ci + toView $ CRRcvFileError user ci e processAgentMessageConn :: forall m. ChatMonad m => User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m () processAgentMessageConn user _ agentConnId END = @@ -2745,21 +2964,23 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do _ -> Nothing processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> m () - processDirectMessage agentMsg connEntity conn@Connection {connId, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case + processDirectMessage agentMsg connEntity conn@Connection {connId, peerChatVRange, viaUserContactLink, groupLinkId, customUserProfileId, connectionCode} = \case Nothing -> case agentMsg of CONF confId _ connInfo -> do -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) let profileToSend = userProfileToSend user (fromLocalProfile <$> incognitoProfile) Nothing - saveConnInfo conn connInfo + conn' <- saveConnInfo conn connInfo -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId $ XInfo profileToSend - INFO connInfo -> - saveConnInfo conn connInfo + allowAgentConnectionAsync user conn' confId $ XInfo profileToSend + INFO connInfo -> do + _conn' <- saveConnInfo conn connInfo + pure () MSG meta _msgFlags msgBody -> do cmdId <- createAckCmd conn - withAckMessage agentConnId cmdId meta $ - saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId $> False + withAckMessage agentConnId cmdId meta $ do + (_conn', _) <- saveRcvMSG conn (ConnectionId connId) meta msgBody cmdId + pure False SENT msgId -> sentMsgDeliveryEvent conn msgId OK -> @@ -2784,62 +3005,71 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do setConnConnReqInv db user connId cReq getXGrpMemIntroContDirect db user ct forM_ contData $ \(hostConnId, xGrpMemIntroCont) -> - sendXGrpMemInv hostConnId directConnReq xGrpMemIntroCont + sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" MSG msgMeta _msgFlags msgBody -> do cmdId <- createAckCmd conn withAckMessage agentConnId cmdId msgMeta $ do - msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId - assertDirectAllowed user MDRcv ct $ toCMEventTag event + (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (ConnectionId connId) msgMeta msgBody cmdId + let ct' = ct {activeConn = conn'} :: Contact + assertDirectAllowed user MDRcv ct' $ toCMEventTag event updateChatLock "directMessage" event case event of - XMsgNew mc -> newContentMessage ct mc msg msgMeta - XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct sharedMsgId fileDescr msgMeta - XMsgFileCancel sharedMsgId -> cancelMessageFile ct sharedMsgId msgMeta - XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct sharedMsgId mContent msg msgMeta ttl live - XMsgDel sharedMsgId _ -> messageDelete ct sharedMsgId msg msgMeta - XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct sharedMsgId reaction add msg msgMeta + XMsgNew mc -> newContentMessage ct' mc msg msgMeta + XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct' sharedMsgId fileDescr msgMeta + XMsgFileCancel sharedMsgId -> cancelMessageFile ct' sharedMsgId msgMeta + XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct' sharedMsgId mContent msg msgMeta ttl live + XMsgDel sharedMsgId _ -> messageDelete ct' sharedMsgId msg msgMeta + XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct' sharedMsgId reaction add msg msgMeta -- TODO discontinue XFile - XFile fInv -> processFileInvitation' ct fInv msg msgMeta - XFileCancel sharedMsgId -> xFileCancel ct sharedMsgId msgMeta - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct sharedMsgId fileConnReq_ fName msgMeta - XInfo p -> xInfo ct p - XGrpInv gInv -> processGroupInvitation ct gInv msg msgMeta - XInfoProbe probe -> xInfoProbe ct probe - XInfoProbeCheck probeHash -> xInfoProbeCheck ct probeHash - XInfoProbeOk probe -> xInfoProbeOk ct probe - XCallInv callId invitation -> xCallInv ct callId invitation msg msgMeta - XCallOffer callId offer -> xCallOffer ct callId offer msg msgMeta - XCallAnswer callId answer -> xCallAnswer ct callId answer msg msgMeta - XCallExtra callId extraInfo -> xCallExtra ct callId extraInfo msg msgMeta - XCallEnd callId -> xCallEnd ct callId msg msgMeta - BFileChunk sharedMsgId chunk -> bFileChunk ct sharedMsgId chunk msgMeta + XFile fInv -> processFileInvitation' ct' fInv msg msgMeta + XFileCancel sharedMsgId -> xFileCancel ct' sharedMsgId msgMeta + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct' sharedMsgId fileConnReq_ fName msgMeta + XInfo p -> xInfo ct' p + XGrpInv gInv -> processGroupInvitation ct' gInv msg msgMeta + XInfoProbe probe -> xInfoProbe (CGMContact ct') probe + XInfoProbeCheck probeHash -> xInfoProbeCheck ct' probeHash + XInfoProbeOk probe -> xInfoProbeOk ct' probe + XCallInv callId invitation -> xCallInv ct' callId invitation msg msgMeta + XCallOffer callId offer -> xCallOffer ct' callId offer msg msgMeta + XCallAnswer callId answer -> xCallAnswer ct' callId answer msg msgMeta + XCallExtra callId extraInfo -> xCallExtra ct' callId extraInfo msg msgMeta + XCallEnd callId -> xCallEnd ct' callId msg msgMeta + BFileChunk sharedMsgId chunk -> bFileChunk ct' sharedMsgId chunk msgMeta _ -> messageError $ "unsupported message: " <> T.pack (show event) - let Contact {chatSettings = ChatSettings {sendRcpts}} = ct + let Contact {chatSettings = ChatSettings {sendRcpts}} = ct' pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event) RCVD msgMeta msgRcpt -> withAckMessage' agentConnId conn msgMeta $ directMsgReceived ct conn msgMeta msgRcpt CONF confId _ connInfo -> do - -- confirming direct connection with a member - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange case chatMsgEvent of + -- confirming direct connection with a member XGrpMemInfo _memId _memProfile -> do -- TODO check member ID -- TODO update member profile -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId XOk - _ -> messageError "CONF from member must have x.grp.mem.info" + allowAgentConnectionAsync user conn' confId XOk + XInfo profile -> do + ct' <- processContactProfileUpdate ct profile False `catchChatError` const (pure ct) + -- [incognito] send incognito profile + incognitoProfile <- forM customUserProfileId $ \profileId -> withStore $ \db -> getProfileById db userId profileId + let p = userProfileToSend user (fromLocalProfile <$> incognitoProfile) (Just ct') + allowAgentConnectionAsync user conn' confId $ XInfo p + void $ withStore' $ \db -> resetMemberContactFields db ct' + _ -> messageError "CONF for existing contact must have x.grp.mem.info or x.info" INFO connInfo -> do - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + _conn' <- updatePeerChatVRange conn chatVRange case chatMsgEvent of XGrpMemInfo _memId _memProfile -> do -- TODO check member ID -- TODO update member profile pure () - XInfo _profile -> do - -- TODO update contact profile - pure () + XInfo profile -> + void $ processContactProfileUpdate ct profile False XOk -> pure () _ -> messageError "INFO for existing contact must have x.grp.mem.info, x.info or x.ok" CON -> @@ -2861,24 +3091,20 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) forM_ groupId_ $ \groupId -> do + subMode <- chatReadVar subscriptionMode gVar <- asks idsDrg - groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation - withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds + groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode + withStore $ \db -> createNewContactMemberAsync db gVar user groupId ct gLinkMemRole groupConnIds (fromJVersionRange peerChatVRange) subMode _ -> pure () - Just (gInfo@GroupInfo {membership}, m@GroupMember {activeConn}) -> + Just (gInfo, m@GroupMember {activeConn}) -> when (maybe False ((== ConnReady) . connStatus) activeConn) $ do notifyMemberConnected gInfo m $ Just ct - let connectedIncognito = contactConnIncognito ct || memberIncognito membership + let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito SENT msgId -> do sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId - withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId msgId) >>= \case - Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _}}) -> pure () - Just (CChatItem SMDSnd ci) -> do - chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId (chatItemId' ci) CISSndSent - toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) - _ -> pure () + updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete SWITCH qd phase cStats -> do toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) when (phase `elem` [SPStarted, SPCompleted]) $ case qd of @@ -2919,10 +3145,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId MERR msgId err -> do - chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId - forM_ chatItemId_ $ \chatItemId -> do - chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId chatItemId (agentErrToItemStatus err) - toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) + updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) incAuthErrCounter connEntity conn err ERR err -> do @@ -2938,22 +3161,30 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do case cReq of groupConnReq@(CRInvitationUri _ _) -> case cmdFunction of -- [async agent commands] XGrpMemIntro continuation on receiving INV - CFCreateConnGrpMemInv -> do - contData <- withStore' $ \db -> do - setConnConnReqInv db user connId cReq - getXGrpMemIntroContGroup db user m - forM_ contData $ \(hostConnId, directConnReq) -> do - let GroupMember {groupMemberId, memberId} = m - sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} + CFCreateConnGrpMemInv + | isCompatibleRange (fromJVersionRange $ peerChatVRange conn) groupNoDirectVRange -> sendWithoutDirectCReq + | otherwise -> sendWithDirectCReq + where + sendWithoutDirectCReq = do + let GroupMember {groupMemberId, memberId} = m + hostConnId <- withStore $ \db -> do + liftIO $ setConnConnReqInv db user connId cReq + getHostConnId db user groupId + sendXGrpMemInv hostConnId Nothing XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} + sendWithDirectCReq = do + let GroupMember {groupMemberId, memberId} = m + contData <- withStore' $ \db -> do + setConnConnReqInv db user connId cReq + getXGrpMemIntroContGroup db user m + forM_ contData $ \(hostConnId, directConnReq) -> + sendXGrpMemInv hostConnId (Just directConnReq) XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} -- [async agent commands] group link auto-accept continuation on receiving INV - CFCreateConnGrpInv -> - withStore' (\db -> getContactViaMember db user m) >>= \case - Nothing -> messageError "implementation error: invitee does not have contact" - Just ct -> do - withStore' $ \db -> setNewContactMemberConnRequest db user m cReq - groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo - sendGrpInvitation ct m groupLinkId - toView $ CRSentGroupInvitation user gInfo ct m + CFCreateConnGrpInv -> do + ct <- withStore $ \db -> getContactViaMember db user m + withStore' $ \db -> setNewContactMemberConnRequest db user m cReq + groupLinkId <- withStore' $ \db -> getGroupLinkId db user gInfo + sendGrpInvitation ct m groupLinkId + toView $ CRSentGroupInvitation user gInfo ct m where sendGrpInvitation :: Contact -> GroupMember -> Maybe GroupLinkId -> m () sendGrpInvitation ct GroupMember {memberId, memberRole = memRole} groupLinkId = do @@ -2965,7 +3196,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do _ -> throwChatError $ CECommandError "unexpected cmdFunction" CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" CONF confId _ connInfo -> do - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange case memberCategory m of GCInviteeMember -> case chatMsgEvent of @@ -2973,7 +3205,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do | sameMemberId memId m -> do withStore $ \db -> liftIO $ updateGroupMemberStatus db userId m GSMemAccepted -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId XOk + allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.grp.acpt: memberId is different from expected" _ -> messageError "CONF from invited member must have x.grp.acpt" _ -> @@ -2982,11 +3214,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do | sameMemberId memId m -> do -- TODO update member profile -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) + allowAgentConnectionAsync user conn' confId $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) | otherwise -> messageError "x.grp.mem.info: memberId is different from expected" _ -> messageError "CONF from member must have x.grp.mem.info" INFO connInfo -> do - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + _conn' <- updatePeerChatVRange conn chatVRange case chatMsgEvent of XGrpMemInfo memId _memProfile | sameMemberId memId m -> do @@ -3027,51 +3260,61 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do processIntro intro `catchChatError` (toView . CRChatError (Just user)) where processIntro intro@GroupMemberIntro {introId} = do - void $ sendDirectMessage conn (XGrpMemIntro . memberInfo $ reMember intro) (GroupId groupId) + void $ sendDirectMessage conn (XGrpMemIntro $ memberInfo (reMember intro)) (GroupId groupId) withStore' $ \db -> updateIntroStatus db introId GMIntroSent _ -> do - -- TODO send probe and decide whether to use existing contact connection or the new contact connection -- TODO notify member who forwarded introduction - question - where it is stored? There is via_contact but probably there should be via_member in group_members table withStore' (\db -> getViaGroupContact db user m) >>= \case Nothing -> do notifyMemberConnected gInfo m Nothing - messageWarning "connected member does not have contact" + let connectedIncognito = memberIncognito membership + when (memberCategory m == GCPreMember) $ probeMatchingMemberContact gInfo m connectedIncognito Just ct@Contact {activeConn = Connection {connStatus}} -> when (connStatus == ConnReady) $ do notifyMemberConnected gInfo m $ Just ct - let connectedIncognito = contactConnIncognito ct || memberIncognito membership + let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo when (memberCategory m == GCPreMember) $ probeMatchingContacts ct connectedIncognito MSG msgMeta _msgFlags msgBody -> do cmdId <- createAckCmd conn withAckMessage agentConnId cmdId msgMeta $ do - msg@RcvMessage {chatMsgEvent = ACME _ event} <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId + (conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveRcvMSG conn (GroupId groupId) msgMeta msgBody cmdId + let m' = m {activeConn = Just conn'} :: GroupMember updateChatLock "groupMessage" event case event of - XMsgNew mc -> canSend $ newGroupContentMessage gInfo m mc msg msgMeta - XMsgFileDescr sharedMsgId fileDescr -> canSend $ groupMessageFileDescription gInfo m sharedMsgId fileDescr msgMeta - XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m sharedMsgId msgMeta - XMsgUpdate sharedMsgId mContent ttl live -> canSend $ groupMessageUpdate gInfo m sharedMsgId mContent msg msgMeta ttl live - XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m sharedMsgId memberId msg msgMeta - XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m sharedMsgId memberId reaction add msg msgMeta + XMsgNew mc -> canSend m' $ newGroupContentMessage gInfo m' mc msg msgMeta + XMsgFileDescr sharedMsgId fileDescr -> canSend m' $ groupMessageFileDescription gInfo m' sharedMsgId fileDescr msgMeta + XMsgFileCancel sharedMsgId -> cancelGroupMessageFile gInfo m' sharedMsgId msgMeta + XMsgUpdate sharedMsgId mContent ttl live -> canSend m' $ groupMessageUpdate gInfo m' sharedMsgId mContent msg msgMeta ttl live + XMsgDel sharedMsgId memberId -> groupMessageDelete gInfo m' sharedMsgId memberId msg msgMeta + XMsgReact sharedMsgId (Just memberId) reaction add -> groupMsgReaction gInfo m' sharedMsgId memberId reaction add msg msgMeta -- TODO discontinue XFile - XFile fInv -> processGroupFileInvitation' gInfo m fInv msg msgMeta - XFileCancel sharedMsgId -> xFileCancelGroup gInfo m sharedMsgId msgMeta - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m sharedMsgId fileConnReq_ fName msgMeta - XGrpMemNew memInfo -> xGrpMemNew gInfo m memInfo msg msgMeta - XGrpMemIntro memInfo -> xGrpMemIntro gInfo m memInfo - XGrpMemInv memId introInv -> xGrpMemInv gInfo m memId introInv - XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m memInfo introInv - XGrpMemRole memId memRole -> xGrpMemRole gInfo m memId memRole msg msgMeta - XGrpMemDel memId -> xGrpMemDel gInfo m memId msg msgMeta - XGrpLeave -> xGrpLeave gInfo m msg msgMeta - XGrpDel -> xGrpDel gInfo m msg msgMeta - XGrpInfo p' -> xGrpInfo gInfo m p' msg msgMeta + XFile fInv -> processGroupFileInvitation' gInfo m' fInv msg msgMeta + XFileCancel sharedMsgId -> xFileCancelGroup gInfo m' sharedMsgId msgMeta + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInvGroup gInfo m' sharedMsgId fileConnReq_ fName msgMeta + XGrpMemNew memInfo -> xGrpMemNew gInfo m' memInfo msg msgMeta + XGrpMemIntro memInfo -> xGrpMemIntro gInfo m' memInfo + XGrpMemInv memId introInv -> xGrpMemInv gInfo m' memId introInv + XGrpMemFwd memInfo introInv -> xGrpMemFwd gInfo m' memInfo introInv + XGrpMemRole memId memRole -> xGrpMemRole gInfo m' memId memRole msg msgMeta + XGrpMemDel memId -> xGrpMemDel gInfo m' memId msg msgMeta + XGrpLeave -> xGrpLeave gInfo m' msg msgMeta + XGrpDel -> xGrpDel gInfo m' msg msgMeta + XGrpInfo p' -> xGrpInfo gInfo m' p' msg msgMeta + XGrpDirectInv connReq mContent_ -> canSend m' $ xGrpDirectInv gInfo m' conn' connReq mContent_ msg msgMeta + XInfoProbe probe -> xInfoProbe (CGMGroupMember gInfo m') probe + -- XInfoProbeCheck -- TODO merge members? + -- XInfoProbeOk -- TODO merge members? BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta _ -> messageError $ "unsupported message: " <> T.pack (show event) - pure False -- no receipts in group now $ hasDeliveryReceipt $ toCMEventTag event + currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo + let GroupInfo {chatSettings = ChatSettings {sendRcpts}} = gInfo + pure $ + fromMaybe (sendRcptsSmallGroups user) sendRcpts + && hasDeliveryReceipt (toCMEventTag event) + && currentMemCount <= smallGroupsRcptsMemLimit where - canSend a - | memberRole (m :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages" + canSend mem a + | memberRole (mem :: GroupMember) <= GRObserver = messageError "member is not allowed to send messages" | otherwise = a RCVD msgMeta msgRcpt -> withAckMessage' agentConnId conn msgMeta $ @@ -3079,6 +3322,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do SENT msgId -> do sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId + updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete SWITCH qd phase cStats -> do toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) when (phase `elem` [SPStarted, SPCompleted]) $ case qd of @@ -3115,7 +3359,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> when (cmdFunction == CFAckMessage) $ ackMsgDeliveryEvent conn cmdId - MERR _ err -> do + MERR msgId err -> do + chatItemId_ <- withStore' $ \db -> getChatItemIdByAgentMsgId db connId msgId + forM_ chatItemId_ $ \itemId -> do + let GroupMember {groupMemberId} = m + updateGroupMemSndStatus itemId groupMemberId $ agentErrToItemStatus err -- group errors are silenced to reduce load on UI event log -- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) incAuthErrCounter connEntity conn err @@ -3151,14 +3399,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do -- SMP CONF for SndFileConnection happens for direct file protocol -- when recipient of the file "joins" connection created by the sender CONF confId _ connInfo -> do - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange case chatMsgEvent of -- TODO save XFileAcpt message XFileAcpt name | name == fileName -> do withStore' $ \db -> updateSndFileStatus db ft FSAccepted -- [async agent commands] no continuation needed, but command should be asynchronous for stability - allowAgentConnectionAsync user conn confId XOk + allowAgentConnectionAsync user conn' confId XOk | otherwise -> messageError "x.file.acpt: fileName is different from expected" _ -> messageError "CONF from file connection must have x.file.acpt" CON -> do @@ -3219,9 +3468,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do -- when sender of the file "joins" connection created by the recipient -- (sender doesn't create connections for all group members) CONF confId _ connInfo -> do - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo + conn' <- updatePeerChatVRange conn chatVRange case chatMsgEvent of - XOk -> allowAgentConnectionAsync user conn confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability + XOk -> allowAgentConnectionAsync user conn' confId XOk -- [async agent commands] no continuation needed, but command should be asynchronous for stability _ -> pure () CON -> startReceivingFile user fileId MSG meta _ msgBody -> do @@ -3255,12 +3505,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do RcvChunkOk -> if B.length chunk /= fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" - else ack $ appendFileChunk ft chunkNo chunk + else ack $ appendFileChunk ft chunkNo chunk False RcvChunkFinal -> if B.length chunk > fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" else do - appendFileChunk ft chunkNo chunk + appendFileChunk ft chunkNo chunk True ci <- withStore $ \db -> do liftIO $ do updateRcvFileStatus db fileId FSComplete @@ -3268,7 +3518,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do deleteRcvFileChunks db ft getChatItemByFileId db user fileId toView $ CRRcvFileComplete user ci - closeFileHandle fileId rcvFiles forM_ conn_ $ \conn -> deleteAgentConnectionAsync user (aConnId conn) RcvChunkDuplicate -> ack $ pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo @@ -3280,10 +3529,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> m () processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of REQ invId _ connInfo -> do - ChatMessage {chatMsgEvent} <- parseChatMessage conn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo case chatMsgEvent of - XContact p xContactId_ -> profileContactRequest invId p xContactId_ - XInfo p -> profileContactRequest invId p Nothing + XContact p xContactId_ -> profileContactRequest invId chatVRange p xContactId_ + XInfo p -> profileContactRequest invId chatVRange p Nothing -- TODO show/log error, other events in contact request _ -> pure () MERR _ err -> do @@ -3295,9 +3544,9 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do -- TODO add debugging output _ -> pure () where - profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m () - profileContactRequest invId p xContactId_ = do - withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case + profileContactRequest :: InvitationId -> VersionRange -> Profile -> Maybe XContactId -> m () + profileContactRequest invId chatVRange p xContactId_ = do + withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId chatVRange p xContactId_) >>= \case CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact CORRequest cReq@UserContactRequest {localDisplayName} -> do withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case @@ -3310,8 +3559,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ct <- acceptContactRequestAsync user cReq incognitoProfile toView $ CRAcceptingContactRequest user ct Just groupId -> do - gInfo@GroupInfo {membership = membership@GroupMember {memberProfile}} <- withStore $ \db -> getGroupInfo db user groupId - let profileMode = if memberIncognito membership then Just $ ExistingIncognito memberProfile else Nothing + gInfo <- withStore $ \db -> getGroupInfo db user groupId + let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo ct <- acceptContactRequestAsync user cReq profileMode toView $ CRAcceptingGroupJoinRequest user gInfo ct _ -> do @@ -3370,7 +3619,6 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do -- 1) retry processing several times -- 2) stabilize database -- 3) show screen of death to the user asking to restart - -- TODO send receipt depending on contact/group settings tryChatError action >>= \case Right withRcpt -> ack $ if withRcpt then Just "" else Nothing Left e -> ack Nothing >> throwError e @@ -3417,22 +3665,45 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do setActive $ ActiveG g showToast ("#" <> g) $ "member " <> c <> " is connected" - probeMatchingContacts :: Contact -> Bool -> m () + probeMatchingContacts :: Contact -> IncognitoEnabled -> m () probeMatchingContacts ct connectedIncognito = do gVar <- asks idsDrg - (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId ct - void . sendDirectContactMessage ct $ XInfoProbe probe if connectedIncognito - then withStore' $ \db -> deleteSentProbe db userId probeId + then sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) else do + (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId (CGMContact ct) + sendProbe probe cs <- withStore' $ \db -> getMatchingContacts db user ct - let probeHash = ProbeHash $ C.sha256Hash (unProbe probe) - forM_ cs $ \c -> sendProbeHash c probeHash probeId `catchChatError` \_ -> pure () + sendProbeHashes cs probe probeId where - sendProbeHash :: Contact -> ProbeHash -> Int64 -> m () - sendProbeHash c probeHash probeId = do + sendProbe :: Probe -> m () + sendProbe probe = void . sendDirectContactMessage ct $ XInfoProbe probe + + probeMatchingMemberContact :: GroupInfo -> GroupMember -> IncognitoEnabled -> m () + probeMatchingMemberContact _ GroupMember {activeConn = Nothing} _ = pure () + probeMatchingMemberContact g m@GroupMember {groupId, activeConn = Just conn} connectedIncognito = do + gVar <- asks idsDrg + if connectedIncognito + then sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) + else do + (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId $ CGMGroupMember g m + sendProbe probe + cs <- withStore' $ \db -> getMatchingMemberContacts db user m + sendProbeHashes cs probe probeId + where + sendProbe :: Probe -> m () + sendProbe probe = void $ sendDirectMessage conn (XInfoProbe probe) (GroupId groupId) + + -- TODO currently we only send probe hashes to contacts + sendProbeHashes :: [Contact] -> Probe -> Int64 -> m () + sendProbeHashes cs probe probeId = + forM_ cs $ \c -> sendProbeHash c `catchChatError` \_ -> pure () + where + probeHash = ProbeHash $ C.sha256Hash (unProbe probe) + sendProbeHash :: Contact -> m () + sendProbeHash c = do void . sendDirectContactMessage c $ XInfoProbeCheck probeHash - withStore' $ \db -> createSentProbeHash db userId probeId c + withStore' $ \db -> createSentProbeHash db userId probeId $ CGMContact c messageWarning :: Text -> m () messageWarning = toView . CRMessageError user "warning" @@ -3492,14 +3763,14 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do processFDMessage fileId fileDescr = do ft <- withStore $ \db -> getRcvFileTransfer db user fileId unless (rcvFileCompleteOrCancelled ft) $ do - (rfd, RcvFileTransfer {fileStatus}) <- withStore $ \db -> do + (rfd, RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs}) <- withStore $ \db -> do rfd <- appendRcvFD db userId fileId fileDescr -- reading second time in the same transaction as appending description -- to prevent race condition with accept ft' <- getRcvFileTransfer db user fileId pure (rfd, ft') - case fileStatus of - RFSAccepted _ -> receiveViaCompleteFD user fileId rfd + case (fileStatus, xftpRcvFile) of + (RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs _ -> pure () cancelMessageFile :: Contact -> SharedMsgId -> MsgMeta -> m () @@ -3525,7 +3796,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do withStore' $ \db -> startRcvInlineFT db user ft fPath inline pure (Just fPath, CIFSRcvAccepted) _ -> pure (Nothing, CIFSRcvInvitation) - pure (ft, CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol}) + let fileSource = CF.plain <$> filePath + pure (ft, CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> m () messageUpdate ct@Contact {contactId, localDisplayName = c} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do @@ -3742,7 +4014,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do inline <- receiveInlineMode fInv Nothing fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} + ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} ci <- saveRcvChatItem' user (CDDirectRcv ct) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) whenContactNtfs user ct $ do @@ -3756,7 +4028,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do inline <- receiveInlineMode fInv Nothing fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvGroupFileTransfer db userId m fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP - ciFile = Just $ CIFile {fileId, fileName, fileSize, filePath = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} + ciFile = Just $ CIFile {fileId, fileName, fileSize, fileSource = Nothing, fileStatus = CIFSRcvInvitation, fileProtocol} ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ msgMeta (CIRcvMsgContent $ MCFile "") ciFile Nothing False groupMsgToView gInfo m ci msgMeta let g = groupName' gInfo @@ -3795,8 +4067,10 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do then unless cancelled $ case fileConnReq_ of -- receiving via a separate connection Just fileConnReq -> do - connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk - withStore' $ \db -> createSndDirectFTConnection db user fileId connIds + subMode <- chatReadVar subscriptionMode + dm <- directMessage XOk + connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode + withStore' $ \db -> createSndDirectFTConnection db user fileId connIds subMode -- receiving inline _ -> do event <- withStore $ \db -> do @@ -3890,10 +4164,12 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do if fName == fileName then unless cancelled $ case (fileConnReq_, activeConn) of (Just fileConnReq, _) -> do + subMode <- chatReadVar subscriptionMode -- receiving via a separate connection -- [async agent commands] no continuation needed, but command should be asynchronous for stability - connIds <- joinAgentConnectionAsync user True fileConnReq $ directMessage XOk - withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m + dm <- directMessage XOk + connIds <- joinAgentConnectionAsync user True fileConnReq dm subMode + withStore' $ \db -> createSndGroupFileTransferConnection db user fileId connIds m subMode (_, Just conn) -> do -- receiving inline event <- withStore $ \db -> do @@ -3915,7 +4191,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> m () processGroupInvitation ct inv msg msgMeta = do - let Contact {localDisplayName = c, activeConn = Connection {customUserProfileId, groupLinkId = groupLinkId'}} = ct + let Contact {localDisplayName = c, activeConn = Connection {peerChatVRange, customUserProfileId, groupLinkId = groupLinkId'}} = ct GroupInvitation {fromMember = (MemberIdRole fromMemId fromRole), invitedMember = (MemberIdRole memId memRole), connRequest, groupLinkId} = inv checkIntegrityCreateItem (CDDirectRcv ct) msgMeta when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) @@ -3924,9 +4200,11 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership = membership@GroupMember {groupMemberId, memberId}}, hostId) <- withStore $ \db -> createGroupInvitation db user ct inv customUserProfileId if sameGroupLinkId groupLinkId groupLinkId' then do - connIds <- joinAgentConnectionAsync user True connRequest . directMessage $ XGrpAcpt memberId + subMode <- chatReadVar subscriptionMode + dm <- directMessage $ XGrpAcpt memberId + connIds <- joinAgentConnectionAsync user True connRequest dm subMode withStore' $ \db -> do - createMemberConnectionAsync db user hostId connIds + createMemberConnectionAsync db user hostId connIds (fromJVersionRange peerChatVRange) subMode updateGroupMemberStatusById db userId hostId GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted toView $ CRUserAcceptedGroupSent user gInfo {membership = membership {memberStatus = GSMemAccepted}} (Just ct) @@ -3935,7 +4213,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ci <- saveRcvChatItem user (CDDirectRcv ct) msg msgMeta content withStore' $ \db -> setGroupInvitationChatItemId db user groupId (chatItemId' ci) toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) - toView $ CRReceivedGroupInvitation user gInfo ct memRole + toView $ CRReceivedGroupInvitation {user, groupInfo = gInfo, contact = ct, fromMemberRole = fromRole, memberRole = memRole} whenContactNtfs user ct $ showToast ("#" <> localDisplayName <> " " <> c <> "> ") "invited you to join the group" where @@ -3949,15 +4227,22 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs) xInfo :: Contact -> Profile -> m () - xInfo c@Contact {profile = p} p' = unless (fromLocalProfile p == p') $ do - c' <- withStore $ \db -> - if userTTL == rcvTTL - then updateContactProfile db user c p' - else do - c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs' - updateContactProfile db user c' p' - when (directOrUsed c') $ createRcvFeatureItems user c c' - toView $ CRContactUpdated user c c' + xInfo c p' = void $ processContactProfileUpdate c p' True + + processContactProfileUpdate :: Contact -> Profile -> Bool -> m Contact + processContactProfileUpdate c@Contact {profile = p} p' createItems + | fromLocalProfile p /= p' = do + c' <- withStore $ \db -> + if userTTL == rcvTTL + then updateContactProfile db user c p' + else do + c' <- liftIO $ updateContactUserPreferences db user c ctUserPrefs' + updateContactProfile db user c' p' + when (directOrUsed c' && createItems) $ createRcvFeatureItems user c c' + toView $ CRContactUpdated user c c' + pure c' + | otherwise = + pure c where Contact {userPreferences = ctUserPrefs@Preferences {timedMessages = ctUserTMPref}} = c userTTL = prefParam $ getPreference SCFTimedMessages ctUserPrefs @@ -3986,35 +4271,48 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do (_, param) = groupFeatureState p createInternalChatItem user (CDGroupRcv g m) (CIRcvGroupFeature (toGroupFeature f) (toGroupPreference p) param) Nothing - xInfoProbe :: Contact -> Probe -> m () - xInfoProbe c2 probe = + xInfoProbe :: ContactOrGroupMember -> Probe -> m () + xInfoProbe cgm2 probe = -- [incognito] unless connected incognito - unless (contactConnIncognito c2) $ do - r <- withStore' $ \db -> matchReceivedProbe db user c2 probe - forM_ r $ \c1 -> probeMatch c1 c2 probe + unless (contactOrGroupMemberIncognito cgm2) $ do + r <- withStore' $ \db -> matchReceivedProbe db user cgm2 probe + forM_ r $ \case + CGMContact c1 -> probeMatch c1 cgm2 probe + CGMGroupMember _ _ -> messageWarning "xInfoProbe ignored: matched member (no probe hashes sent to members)" + -- TODO currently we send probe hashes only to contacts xInfoProbeCheck :: Contact -> ProbeHash -> m () xInfoProbeCheck c1 probeHash = -- [incognito] unless connected incognito unless (contactConnIncognito c1) $ do - r <- withStore' $ \db -> matchReceivedProbeHash db user c1 probeHash + r <- withStore' $ \db -> matchReceivedProbeHash db user (CGMContact c1) probeHash forM_ r . uncurry $ probeMatch c1 - probeMatch :: Contact -> Contact -> Probe -> m () - probeMatch c1@Contact {contactId = cId1, profile = p1} c2@Contact {contactId = cId2, profile = p2} probe = - if profilesMatch (fromLocalProfile p1) (fromLocalProfile p2) && cId1 /= cId2 - then do - void . sendDirectContactMessage c1 $ XInfoProbeOk probe - mergeContacts c1 c2 - else messageWarning "probeMatch ignored: profiles don't match or same contact id" + probeMatch :: Contact -> ContactOrGroupMember -> Probe -> m () + probeMatch c1@Contact {contactId = cId1, profile = p1} cgm2 probe = + case cgm2 of + CGMContact c2@Contact {contactId = cId2, profile = p2} + | cId1 /= cId2 && profilesMatch p1 p2 -> do + void . sendDirectContactMessage c1 $ XInfoProbeOk probe + mergeContacts c1 c2 + | otherwise -> messageWarning "probeMatch ignored: profiles don't match or same contact id" + CGMGroupMember g m2@GroupMember {memberProfile = p2, memberContactId} + | isNothing memberContactId && profilesMatch p1 p2 -> do + void . sendDirectContactMessage c1 $ XInfoProbeOk probe + connectContactToMember c1 g m2 + | otherwise -> messageWarning "probeMatch ignored: profiles don't match or member already has contact" + -- TODO currently we send probe hashes only to contacts xInfoProbeOk :: Contact -> Probe -> m () - xInfoProbeOk c1@Contact {contactId = cId1} probe = do - r <- withStore' $ \db -> matchSentProbe db user c1 probe - forM_ r $ \c2@Contact {contactId = cId2} -> - if cId1 /= cId2 - then mergeContacts c1 c2 - else messageWarning "xInfoProbeOk ignored: same contact id" + xInfoProbeOk c1@Contact {contactId = cId1} probe = + withStore' (\db -> matchSentProbe db user (CGMContact c1) probe) >>= \case + Just (CGMContact c2@Contact {contactId = cId2}) + | cId1 /= cId2 -> mergeContacts c1 c2 + | otherwise -> messageWarning "xInfoProbeOk ignored: same contact id" + Just (CGMGroupMember g m2@GroupMember {memberContactId}) + | isNothing memberContactId -> connectContactToMember c1 g m2 + | otherwise -> messageWarning "xInfoProbeOk ignored: member already has contact" + _ -> pure () -- to party accepting call xCallInv :: Contact -> CallId -> CallInvitation -> RcvMessage -> MsgMeta -> m () @@ -4126,18 +4424,25 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do withStore' $ \db -> mergeContactRecords db userId c1 c2 toView $ CRContactsMerged user c1 c2 - saveConnInfo :: Connection -> ConnInfo -> m () + connectContactToMember :: Contact -> GroupInfo -> GroupMember -> m () + connectContactToMember c1 g m2 = do + withStore' $ \db -> updateMemberContact db user c1 m2 + toView $ CRMemberContactConnected user c1 g m2 + + saveConnInfo :: Connection -> ConnInfo -> m Connection saveConnInfo activeConn connInfo = do - ChatMessage {chatMsgEvent} <- parseChatMessage activeConn connInfo + ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage activeConn connInfo + conn' <- updatePeerChatVRange activeConn chatVRange case chatMsgEvent of XInfo p -> do - ct <- withStore $ \db -> createDirectContact db user activeConn p + ct <- withStore $ \db -> createDirectContact db user conn' p toView $ CRContactConnecting user ct + pure conn' -- TODO show/log error, other events in SMP confirmation - _ -> pure () + _ -> pure conn' xGrpMemNew :: GroupInfo -> GroupMember -> MemberInfo -> RcvMessage -> MsgMeta -> m () - xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole memberProfile) msg msgMeta = do + xGrpMemNew gInfo m memInfo@(MemberInfo memId memRole _ memberProfile) msg msgMeta = do checkHostRole m memRole members <- withStore' $ \db -> getGroupMembers db user gInfo unless (sameMemberId memId $ membership gInfo) $ @@ -4150,7 +4455,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do toView $ CRJoinedGroupMemberConnecting user gInfo m newMember xGrpMemIntro :: GroupInfo -> GroupMember -> MemberInfo -> m () - xGrpMemIntro gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ _) = do + xGrpMemIntro gInfo@GroupInfo {chatSettings = ChatSettings {enableNtfs}} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memberChatVRange _) = do case memberCategory m of GCHostMember -> do members <- withStore' $ \db -> getGroupMembers db user gInfo @@ -4158,15 +4463,21 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do then messageWarning "x.grp.mem.intro ignored: member already exists" else do when (memberRole < GRAdmin) $ throwChatError (CEGroupContactRole c) + subMode <- chatReadVar subscriptionMode -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second - groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation - directConnIds <- createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation - -- [incognito] direct connection with member has to be established using the same incognito profile [that was known to host and used for group membership] - let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing - void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId + groupConnIds <- createConn subMode + directConnIds <- case memberChatVRange of + Nothing -> Just <$> createConn subMode + Just mcvr + | isCompatibleRange (fromChatVRange mcvr) groupNoDirectVRange -> pure Nothing + | otherwise -> Just <$> createConn subMode + let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo + void $ withStore $ \db -> createIntroReMember db user gInfo m memInfo groupConnIds directConnIds customUserProfileId subMode _ -> messageError "x.grp.mem.intro can be only sent by host member" + where + createConn subMode = createAgentConnectionAsync user CFCreateConnGrpMemInv enableNtfs SCMInvitation subMode - sendXGrpMemInv :: Int64 -> ConnReqInvitation -> XGrpMemIntroCont -> m () + sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> m () sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do hostConn <- withStore $ \db -> getConnectionById db user hostConnId let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq} @@ -4187,7 +4498,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do _ -> messageError "x.grp.mem.inv can be only sent by invitee member" xGrpMemFwd :: GroupInfo -> GroupMember -> MemberInfo -> IntroInvitation -> m () - xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole _) introInv@IntroInvitation {groupConnReq, directConnReq} = do + xGrpMemFwd gInfo@GroupInfo {membership, chatSettings = ChatSettings {enableNtfs}} m memInfo@(MemberInfo memId memRole memberChatVRange _) introInv@IntroInvitation {groupConnReq, directConnReq} = do checkHostRole m memRole members <- withStore' $ \db -> getGroupMembers db user gInfo toMember <- case find (sameMemberId memId) members of @@ -4198,13 +4509,15 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do Nothing -> withStore $ \db -> createNewGroupMember db user gInfo memInfo GCPostMember GSMemAnnounced Just m' -> pure m' withStore' $ \db -> saveMemberInvitation db toMember introInv + subMode <- chatReadVar subscriptionMode -- [incognito] send membership incognito profile, create direct connection as incognito - let msg = XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) + dm <- directMessage $ XGrpMemInfo (memberId (membership :: GroupMember)) (fromLocalProfile $ memberProfile membership) -- [async agent commands] no continuation needed, but commands should be asynchronous for stability - groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq $ directMessage msg - directConnIds <- joinAgentConnectionAsync user enableNtfs directConnReq $ directMessage msg - let customUserProfileId = if memberIncognito membership then Just (localProfileId $ memberProfile membership) else Nothing - withStore' $ \db -> createIntroToMemberContact db user m toMember groupConnIds directConnIds customUserProfileId + groupConnIds <- joinAgentConnectionAsync user enableNtfs groupConnReq dm subMode + directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user enableNtfs dcr dm subMode + let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo + mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange + withStore' $ \db -> createIntroToMemberContact db user m toMember mcvr groupConnIds directConnIds customUserProfileId subMode xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> MsgMeta -> m () xGrpMemRole gInfo@GroupInfo {membership} m@GroupMember {memberRole = senderRole} memId memRole msg msgMeta @@ -4296,22 +4609,108 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do groupMsgToView g' m ci msgMeta createGroupFeatureChangedItems user cd CIRcvGroupFeature g g' + xGrpDirectInv :: GroupInfo -> GroupMember -> Connection -> ConnReqInvitation -> Maybe MsgContent -> RcvMessage -> MsgMeta -> m () + xGrpDirectInv g m mConn connReq mContent_ msg msgMeta = do + unless (groupFeatureAllowed SGFDirectMessages g) $ messageError "x.grp.direct.inv: direct messages not allowed" + let GroupMember {memberContactId} = m + subMode <- chatReadVar subscriptionMode + case memberContactId of + Nothing -> createNewContact subMode + Just mContactId -> do + mCt <- withStore $ \db -> getContact db user mContactId + let Contact {activeConn = Connection {connId}, contactGrpInvSent} = mCt + if contactGrpInvSent + then do + ownConnReq <- withStore $ \db -> getConnReqInv db connId + -- in case both members sent x.grp.direct.inv before receiving other's for processing, + -- only the one who received greater connReq joins, the other creates items and waits for confirmation + if strEncode connReq > strEncode ownConnReq + then joinExistingContact subMode mCt + else createItems mCt m + else joinExistingContact subMode mCt + where + joinExistingContact subMode mCt = do + connIds <- joinConn subMode + mCt' <- withStore' $ \db -> updateMemberContactInvited db user connIds g mConn mCt subMode + createItems mCt' m + securityCodeChanged mCt' + createNewContact subMode = do + connIds <- joinConn subMode + -- [incognito] reuse membership incognito profile + (mCt', m') <- withStore' $ \db -> createMemberContactInvited db user connIds g m mConn subMode + createItems mCt' m' + joinConn subMode = do + -- [incognito] send membership incognito profile + let p = userProfileToSend user (fromLocalProfile <$> incognitoMembershipProfile g) Nothing + dm <- directMessage $ XInfo p + joinAgentConnectionAsync user True connReq dm subMode + createItems mCt' m' = do + checkIntegrityCreateItem (CDGroupRcv g m') msgMeta + createInternalChatItem user (CDGroupRcv g m') (CIRcvGroupEvent RGEMemberCreatedContact) Nothing + toView $ CRNewMemberContactReceivedInv user mCt' g m' + forM_ mContent_ $ \mc -> do + ci <- saveRcvChatItem user (CDDirectRcv mCt') msg msgMeta (CIRcvMsgContent mc) + toView $ CRNewChatItem user (AChatItem SCTDirect SMDRcv (DirectChat mCt') ci) + securityCodeChanged ct = do + toView $ CRContactVerificationReset user ct + createInternalChatItem user (CDDirectRcv ct) (CIRcvConnEvent RCEVerificationCodeReset) Nothing + directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m () - directMsgReceived ct@Contact {contactId} Connection {connId} msgMeta msgRcpts = do + directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do checkIntegrityCreateItem (CDDirectRcv ct) msgMeta forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do withStore $ \db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus - withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId agentMsgId) >>= \case - Just (CChatItem SMDSnd ci) -> do - chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId (chatItemId' ci) $ CISSndRcvd msgRcptStatus - toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) - _ -> pure () + updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m () - groupMsgReceived gInfo m Connection {connId} msgMeta msgRcpts = do + groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta - forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> + forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do withStore $ \db -> createSndMsgDeliveryEvent db connId agentMsgId $ MDSSndRcvd msgRcptStatus + updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete + + updateDirectItemStatus :: Contact -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m () + updateDirectItemStatus ct@Contact {contactId} Connection {connId} msgId newStatus = + withStore' (\db -> getDirectChatItemByAgentMsgId db user contactId connId msgId) >>= \case + Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ _}}) -> pure () + Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) + | itemStatus == newStatus -> pure () + | otherwise -> do + chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId itemId newStatus + toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) + _ -> pure () + + updateGroupMemSndStatus :: ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> m Bool + updateGroupMemSndStatus itemId groupMemberId newStatus = + runExceptT (withStore $ \db -> getGroupSndStatus db itemId groupMemberId) >>= \case + Right (CISSndRcvd _ _) -> pure False + Right memStatus + | memStatus == newStatus -> pure False + | otherwise -> withStore' (\db -> updateGroupSndStatus db itemId groupMemberId newStatus) $> True + _ -> pure False + + updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> m () + updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus = + withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case + Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure () + Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do + memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus + when memStatusChanged $ do + memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId) + let newStatus = membersGroupItemStatus memStatusCounts + when (newStatus /= itemStatus) $ do + chatItem <- withStore $ \db -> updateGroupChatItemStatus db user groupId itemId newStatus + toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem) + _ -> pure () + +updatePeerChatVRange :: ChatMonad m => Connection -> VersionRange -> m Connection +updatePeerChatVRange conn@Connection {connId, peerChatVRange} msgChatVRange = do + let jMsgChatVRange = JVersionRange msgChatVRange + if jMsgChatVRange /= peerChatVRange + then do + withStore' $ \db -> setPeerChatVRange db connId msgChatVRange + pure conn {peerChatVRange = jMsgChatVRange} + else pure conn parseFileDescription :: (ChatMonad m, FilePartyI p) => Text -> m (ValidFileDescription p) parseFileDescription = @@ -4387,8 +4786,8 @@ readFileChunk SndFileTransfer {fileId, filePath, chunkSize} chunkNo = do parseFileChunk :: ChatMonad m => ByteString -> m FileChunk parseFileChunk = liftEither . first (ChatError . CEFileRcvChunk) . smpDecode -appendFileChunk :: ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> m () -appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = +appendFileChunk :: forall m. ChatMonad m => RcvFileTransfer -> Integer -> ByteString -> Bool -> m () +appendFileChunk ft@RcvFileTransfer {fileId, fileInvitation, fileStatus, cryptoArgs} chunkNo chunk final = case fileStatus of RFSConnected RcvFileInfo {filePath} -> append_ filePath -- sometimes update of file transfer status to FSConnected @@ -4397,11 +4796,27 @@ appendFileChunk ft@RcvFileTransfer {fileId, fileStatus} chunkNo chunk = RFSCancelled _ -> pure () _ -> throwChatError $ CEFileInternal "receiving file transfer not in progress" where + append_ :: FilePath -> m () append_ filePath = do fsFilePath <- toFSFilePath filePath h <- getFileHandle fileId fsFilePath rcvFiles AppendMode - liftIO (B.hPut h chunk >> hFlush h) `catchThrow` (ChatError . CEFileWrite filePath . show) + liftIO (B.hPut h chunk >> hFlush h) `catchThrow` (fileErr . show) withStore' $ \db -> updatedRcvFileChunkStored db ft chunkNo + when final $ do + closeFileHandle fileId rcvFiles + forM_ cryptoArgs $ \cfArgs -> do + tmpFile <- getChatTempDirectory >>= (`uniqueCombine` fileName (fileInvitation :: FileInvitation)) + tryChatError (liftError encryptErr $ encryptFile fsFilePath tmpFile cfArgs) >>= \case + Right () -> do + removeFile fsFilePath `catchChatError` \_ -> pure () + renameFile tmpFile fsFilePath + Left e -> do + toView $ CRChatError Nothing e + removeFile tmpFile `catchChatError` \_ -> pure () + withStore' (`removeFileCryptoArgs` fileId) + where + encryptErr e = fileErr $ e <> ", received file not encrypted" + fileErr = ChatError . CEFileWrite filePath getFileHandle :: ChatMonad m => Int64 -> FilePath -> (ChatController -> TVar (Map Int64 Handle)) -> IOMode -> m Handle getFileHandle fileId filePath files ioMode = do @@ -4511,12 +4926,15 @@ sendDirectMessage conn chatMsgEvent connOrGroupId = do createSndMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> ConnOrGroupId -> m SndMessage createSndMessage chatMsgEvent connOrGroupId = do gVar <- asks idsDrg + ChatConfig {chatVRange} <- asks config withStore $ \db -> createNewSndMessage db gVar connOrGroupId $ \sharedMsgId -> - let msgBody = strEncode ChatMessage {msgId = Just sharedMsgId, chatMsgEvent} + let msgBody = strEncode ChatMessage {chatVRange, msgId = Just sharedMsgId, chatMsgEvent} in NewMessage {chatMsgEvent, msgBody} -directMessage :: MsgEncodingI e => ChatMsgEvent e -> ByteString -directMessage chatMsgEvent = strEncode ChatMessage {msgId = Nothing, chatMsgEvent} +directMessage :: (MsgEncodingI e, ChatMonad m) => ChatMsgEvent e -> m ByteString +directMessage chatMsgEvent = do + ChatConfig {chatVRange} <- asks config + pure $ strEncode ChatMessage {chatVRange, msgId = Nothing, chatMsgEvent} deliverMessage :: ChatMonad m => Connection -> CMEventTag e -> MsgBody -> MessageId -> m Int64 deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do @@ -4527,26 +4945,33 @@ deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do (Just $ "createSndMsgDelivery, sndMsgDelivery: " <> show sndMsgDelivery <> ", msgId: " <> show msgId <> ", cmEventTag: " <> show cmEventTag <> ", msgDeliveryStatus: MDSSndAgent") $ \db -> createSndMsgDelivery db sndMsgDelivery msgId -sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m SndMessage +sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember]) sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = sendGroupMessage' user members chatMsgEvent groupId Nothing $ pure () -sendGroupMessage' :: (MsgEncodingI e, ChatMonad m) => User -> [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m SndMessage +sendGroupMessage' :: forall e m. (MsgEncodingI e, ChatMonad m) => User -> [GroupMember] -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> m () -> m (SndMessage, [GroupMember]) sendGroupMessage' user members chatMsgEvent groupId introId_ postDeliver = do msg <- createSndMessage chatMsgEvent (GroupId groupId) -- TODO collect failed deliveries into a single error - forM_ (filter memberCurrent members) $ \m -> - messageMember m msg `catchChatError` (toView . CRChatError (Just user)) - pure msg + rs <- forM (filter memberCurrent members) $ \m -> + messageMember m msg `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) + let sentToMembers = catMaybes rs + pure (msg, sentToMembers) where + messageMember :: GroupMember -> SndMessage -> m (Maybe GroupMember) messageMember m@GroupMember {groupMemberId} SndMessage {msgId, msgBody} = case memberConn m of - Nothing -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ + Nothing -> do + withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ + pure $ Just m Just conn@Connection {connStatus} - | connDisabled conn || connStatus == ConnDeleted -> pure () + | connDisabled conn || connStatus == ConnDeleted -> pure Nothing | connStatus == ConnSndReady || connStatus == ConnReady -> do let tag = toCMEventTag chatMsgEvent deliverMessage conn tag msgBody msgId >> postDeliver - | otherwise -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ + pure $ Just m + | otherwise -> do + withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ + pure $ Just m sendPendingGroupMessages :: ChatMonad m => User -> GroupMember -> Connection -> m () sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do @@ -4564,15 +4989,17 @@ sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn _ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName _ -> pure () -saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m RcvMessage +saveRcvMSG :: ChatMonad m => Connection -> ConnOrGroupId -> MsgMeta -> MsgBody -> CommandId -> m (Connection, RcvMessage) saveRcvMSG conn@Connection {connId} connOrGroupId agentMsgMeta msgBody agentAckCmdId = do - ACMsg _ ChatMessage {msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody + ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} <- parseAChatMessage conn agentMsgMeta msgBody + conn' <- updatePeerChatVRange conn chatVRange let agentMsgId = fst $ recipient agentMsgMeta newMsg = NewMessage {chatMsgEvent, msgBody} rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta, agentAckCmdId} - withStoreCtx' + msg <- withStoreCtx' (Just $ "createNewMessageAndRcvMsgDelivery, rcvMsgDelivery: " <> show rcvMsgDelivery <> ", sharedMsgId_: " <> show sharedMsgId_ <> ", msgDeliveryStatus: MDSRcvAgent") $ \db -> createNewMessageAndRcvMsgDelivery db connOrGroupId newMsg sharedMsgId_ rcvMsgDelivery + pure (conn', msg) saveSndChatItem :: ChatMonad m => User -> ChatDirection c 'MDSnd -> SndMessage -> CIContent 'MDSnd -> m (ChatItem c 'MDSnd) saveSndChatItem user cd msg content = saveSndChatItem' user cd msg content Nothing Nothing Nothing False @@ -4624,10 +5051,9 @@ deleteGroupCI user gInfo ci@(CChatItem msgDir deletedItem@ChatItem {file}) byUse pure $ CRChatItemDeleted user (AChatItem SCTGroup msgDir (GroupChat gInfo) deletedItem) toCi byUser timed deleteCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () -deleteCIFile user file = - forM_ file $ \CIFile {fileId, filePath, fileStatus} -> do - let fileInfo = CIFileInfo {fileId, fileStatus = Just $ AFS msgDirection fileStatus, filePath} - fileAgentConnIds <- deleteFile' user fileInfo True +deleteCIFile user file_ = + forM_ file_ $ \file -> do + fileAgentConnIds <- deleteFile' user (mkCIFileInfo file) True deleteAgentConnectionsAsync user fileAgentConnIds markDirectCIDeleted :: ChatMonad m => User -> Contact -> CChatItem 'CTDirect -> MessageId -> Bool -> UTCTime -> m ChatResponse @@ -4651,34 +5077,35 @@ markGroupCIDeleted user gInfo@GroupInfo {groupId} ci@(CChatItem _ ChatItem {file gItem (CChatItem msgDir ci') = AChatItem SCTGroup msgDir (GroupChat gInfo) ci' cancelCIFile :: (ChatMonad m, MsgDirectionI d) => User -> Maybe (CIFile d) -> m () -cancelCIFile user file = - forM_ file $ \CIFile {fileId, filePath, fileStatus} -> do - let fileInfo = CIFileInfo {fileId, fileStatus = Just $ AFS msgDirection fileStatus, filePath} - fileAgentConnIds <- cancelFile' user fileInfo True +cancelCIFile user file_ = + forM_ file_ $ \file -> do + fileAgentConnIds <- cancelFile' user (mkCIFileInfo file) True deleteAgentConnectionsAsync user fileAgentConnIds -createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> m (CommandId, ConnId) -createAgentConnectionAsync user cmdFunction enableNtfs cMode = do +createAgentConnectionAsync :: forall m c. (ChatMonad m, ConnectionModeI c) => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> m (CommandId, ConnId) +createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing cmdFunction - connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode + connId <- withAgent $ \a -> createConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cMode subMode pure (cmdId, connId) -joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> m (CommandId, ConnId) -joinAgentConnectionAsync user enableNtfs cReqUri cInfo = do +joinAgentConnectionAsync :: ChatMonad m => User -> Bool -> ConnectionRequestUri c -> ConnInfo -> SubscriptionMode -> m (CommandId, ConnId) +joinAgentConnectionAsync user enableNtfs cReqUri cInfo subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFJoinConn - connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo + connId <- withAgent $ \a -> joinConnectionAsync a (aUserId user) (aCorrId cmdId) enableNtfs cReqUri cInfo subMode pure (cmdId, connId) allowAgentConnectionAsync :: (MsgEncodingI e, ChatMonad m) => User -> Connection -> ConfirmationId -> ChatMsgEvent e -> m () allowAgentConnectionAsync user conn@Connection {connId} confId msg = do cmdId <- withStore' $ \db -> createCommand db user (Just connId) CFAllowConn - withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId $ directMessage msg + dm <- directMessage msg + withAgent $ \a -> allowConnectionAsync a (aCorrId cmdId) (aConnId conn) confId dm withStore' $ \db -> updateConnectionStatus db conn ConnAccepted -agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> m (CommandId, ConnId) -agentAcceptContactAsync user enableNtfs invId msg = do +agentAcceptContactAsync :: (MsgEncodingI e, ChatMonad m) => User -> Bool -> InvitationId -> ChatMsgEvent e -> SubscriptionMode -> m (CommandId, ConnId) +agentAcceptContactAsync user enableNtfs invId msg subMode = do cmdId <- withStore' $ \db -> createCommand db user Nothing CFAcceptContact - connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId $ directMessage msg + dm <- directMessage msg + connId <- withAgent $ \a -> acceptContactAsync a (aCorrId cmdId) enableNtfs invId dm subMode pure (cmdId, connId) deleteAgentConnectionAsync :: ChatMonad m => User -> ConnId -> m () @@ -4773,13 +5200,13 @@ createInternalChatItem user cd content itemTs_ = do ci <- liftIO $ mkChatItem cd ciId content Nothing Nothing Nothing Nothing False itemTs createdAt toView $ CRNewChatItem user (AChatItem (chatTypeI @c) (msgDirection @d) (toChatInfo cd) ci) -getCreateActiveUser :: SQLiteStore -> IO User -getCreateActiveUser st = do +getCreateActiveUser :: SQLiteStore -> Bool -> IO User +getCreateActiveUser st testView = do user <- withTransaction st getUsers >>= \case [] -> newUser users -> maybe (selectUser users) pure (find activeUser users) - putStrLn $ "Current user: " <> userStr user + unless testView $ putStrLn $ "Current user: " <> userStr user pure user where newUser :: IO User @@ -4887,7 +5314,7 @@ withAgent :: ChatMonad m => (AgentClient -> ExceptT AgentErrorType m a) -> m a withAgent action = asks smpAgent >>= runExceptT . action - >>= liftEither . first (\e -> ChatErrorAgent e Nothing) + >>= liftEither . first (`ChatErrorAgent` Nothing) withStore' :: ChatMonad m => (DB.Connection -> IO a) -> m a withStore' action = withStore $ liftIO . action @@ -4928,8 +5355,10 @@ chatCommandP = "/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)), ("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)), "/set receipts all " *> (SetAllContactReceipts <$> onOffP), - "/_set receipts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings), - "/set receipts " *> (SetUserContactReceipts <$> receiptSettings), + "/_set receipts contacts " *> (APISetUserContactReceipts <$> A.decimal <* A.space <*> receiptSettings), + "/set receipts contacts " *> (SetUserContactReceipts <$> receiptSettings), + "/_set receipts groups " *> (APISetUserGroupReceipts <$> A.decimal <* A.space <*> receiptSettings), + "/set receipts groups " *> (SetUserGroupReceipts <$> receiptSettings), "/_hide user " *> (APIHideUser <$> A.decimal <* A.space <*> jsonP), "/_unhide user " *> (APIUnhideUser <$> A.decimal <* A.space <*> jsonP), "/_mute user " *> (APIMuteUser <$> A.decimal), @@ -4961,6 +5390,7 @@ chatCommandP = "/db decrypt " *> (APIStorageEncryption . (`DBEncryptionConfig` "") <$> dbKeyP), "/sql chat " *> (ExecChatStoreSQL <$> textP), "/sql agent " *> (ExecAgentStoreSQL <$> textP), + "/sql slow" $> SlowSQLQueries, "/_get chats " *> (APIGetChats <$> A.decimal <*> (" pcc=on" $> True <|> " pcc=off" $> False <|> pure False)), "/_get chat " *> (APIGetChat <$> chatRefP <* A.space <*> chatPaginationP <*> optional (" search=" *> stringP)), "/_get items " *> (APIGetChatItems <$> chatPaginationP <*> optional (" search=" *> stringP)), @@ -4974,7 +5404,7 @@ chatCommandP = "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), "/_delete " *> (APIDeleteChat <$> chatRefP), "/_clear chat " *> (APIClearChat <$> chatRefP), - "/_accept " *> (APIAcceptContact <$> A.decimal), + "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), "/_reject " *> (APIRejectContact <$> A.decimal), "/_call invite @" *> (APISendCallInvitation <$> A.decimal <* A.space <*> jsonP), "/call " *> char_ '@' *> (SendCallInvitation <$> displayName <*> pure defaultCallType), @@ -5022,8 +5452,10 @@ chatCommandP = "/reconnect" $> ReconnectAllServers, "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), "/_info #" *> (APIGroupMemberInfo <$> A.decimal <* A.space <*> A.decimal), + "/_info #" *> (APIGroupInfo <$> A.decimal), "/_info @" *> (APIContactInfo <$> A.decimal), ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName), + ("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayName), ("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName), "/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), "/_switch @" *> (APISwitchContact <$> A.decimal), @@ -5053,13 +5485,14 @@ chatCommandP = ("/help groups" <|> "/help group" <|> "/hg") $> ChatHelp HSGroups, ("/help contacts" <|> "/help contact" <|> "/hc") $> ChatHelp HSContacts, ("/help address" <|> "/ha") $> ChatHelp HSMyAddress, + "/help incognito" $> ChatHelp HSIncognito, ("/help messages" <|> "/hm") $> ChatHelp HSMessages, ("/help settings" <|> "/hs") $> ChatHelp HSSettings, ("/help db" <|> "/hd") $> ChatHelp HSDatabase, ("/help" <|> "/h") $> ChatHelp HSMain, ("/group " <|> "/g ") *> char_ '#' *> (NewGroup <$> groupProfile), "/_group " *> (APINewGroup <$> A.decimal <* A.space <*> jsonP), - ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRAdmin)), + ("/add " <|> "/a ") *> char_ '#' *> (AddMember <$> displayName <* A.space <* char_ '@' <*> displayName <*> (memberRole <|> pure GRMember)), ("/join " <|> "/j ") *> char_ '#' *> (JoinGroup <$> displayName), ("/member role " <|> "/mr ") *> char_ '#' *> (MemberRole <$> displayName <* A.space <* char_ '@' <*> displayName <*> memberRole), ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName), @@ -5069,11 +5502,15 @@ chatCommandP = "/clear #" *> (ClearGroup <$> displayName), "/clear " *> char_ '@' *> (ClearContact <$> displayName), ("/members " <|> "/ms ") *> char_ '#' *> (ListMembers <$> displayName), - ("/groups" <|> "/gs") $> ListGroups, + "/_groups" *> (APIListGroups <$> A.decimal <*> optional (" @" *> A.decimal) <*> optional (A.space *> stringP)), + ("/groups" <|> "/gs") *> (ListGroups <$> optional (" @" *> displayName) <*> optional (A.space *> stringP)), "/_group_profile #" *> (APIUpdateGroupProfile <$> A.decimal <* A.space <*> jsonP), ("/group_profile " <|> "/gp ") *> char_ '#' *> (UpdateGroupNames <$> displayName <* A.space <*> groupProfile), ("/group_profile " <|> "/gp ") *> char_ '#' *> (ShowGroupProfile <$> displayName), "/group_descr " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> optional (A.space *> msgTextP)), + "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayName <* A.space <*> (Just <$> msgTextP)), + "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayName <*> pure Nothing), + "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayName), "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), @@ -5082,15 +5519,19 @@ chatCommandP = "/set link role #" *> (GroupLinkMemberRole <$> displayName <*> memberRole), "/delete link #" *> (DeleteGroupLink <$> displayName), "/show link #" *> (ShowGroupLink <$> displayName), + "/_create member contact #" *> (APICreateMemberContact <$> A.decimal <* A.space <*> A.decimal), + "/_invite member contact @" *> (APISendMemberContactInvitation <$> A.decimal <*> optional (A.space *> msgContentP)), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <*> pure Nothing <*> quotedMsg <*> msgTextP), (">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP), "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, - "/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), - "/_connect " *> (APIAddContact <$> A.decimal), - ("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)), - ("/connect" <|> "/c") $> AddContact, + "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), + "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), + "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), + ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), + ("/connect" <|> "/c") *> (AddContact <$> incognitoP), SendMessage <$> chatNameP <* A.space <*> msgTextP, + "@#" *> (SendMemberContactMessage <$> displayName <* A.space <* char_ '@' <*> displayName <* A.space <*> msgTextP), "/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")), (">@" <|> "> @") *> sendMsgQuote (AMsgDirection SMDRcv), (">>@" <|> ">> @") *> sendMsgQuote (AMsgDirection SMDSnd), @@ -5111,11 +5552,11 @@ chatCommandP = ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), - ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), - "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal), + ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), + "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" encrypt=" *> onOffP <|> pure False)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), - "/simplex" $> ConnectSimplex, + "/simplex" *> (ConnectSimplex <$> incognitoP), "/_address " *> (APICreateMyAddress <$> A.decimal), ("/address" <|> "/ad") $> CreateMyAddress, "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), @@ -5126,7 +5567,7 @@ chatCommandP = ("/profile_address " <|> "/pa ") *> (SetProfileAddress <$> onOffP), "/_auto_accept " *> (APIAddressAutoAccept <$> A.decimal <* A.space <*> autoAcceptP), "/auto_accept " *> (AddressAutoAccept <$> autoAcceptP), - ("/accept " <|> "/ac ") *> char_ '@' *> (AcceptContact <$> displayName), + ("/accept" <|> "/ac") *> (AcceptContact <$> incognitoP <* A.space <* char_ '@' <*> displayName), ("/reject " <|> "/rc ") *> char_ '@' *> (RejectContact <$> displayName), ("/markdown" <|> "/m") $> ChatHelp HSMarkdown, ("/welcome" <|> "/w") $> Welcome, @@ -5148,15 +5589,19 @@ chatCommandP = "/set disappear #" *> (SetGroupTimedMessages <$> displayName <*> (A.space *> timedTTLOnOffP)), "/set disappear @" *> (SetContactTimedMessages <$> displayName <*> optional (A.space *> timedMessagesEnabledP)), "/set disappear " *> (SetUserTimedMessages <$> (("yes" $> True) <|> ("no" $> False))), - "/incognito " *> (SetIncognito <$> onOffP), + ("/incognito" <* optional (A.space *> onOffP)) $> ChatHelp HSIncognito, ("/quit" <|> "/q" <|> "/exit") $> QuitChat, ("/version" <|> "/v") $> ShowVersion, "/debug locks" $> DebugLocks, "/get stats" $> GetAgentStats, - "/reset stats" $> ResetAgentStats + "/reset stats" $> ResetAgentStats, + "/get subs" $> GetAgentSubs, + "/get subs details" $> GetAgentSubsDetails ] where choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) + incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False + incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) chatTypeP = A.char '@' $> CTDirect <|> A.char '#' $> CTGroup <|> A.char ':' $> CTContactConnection diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index 05a755fd87..df9c66ceee 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -9,9 +9,7 @@ module Simplex.Chat.Bot where import Control.Concurrent.Async import Control.Concurrent.STM import Control.Monad.Reader -import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B -import qualified Data.ByteString.Lazy.Char8 as LB import qualified Data.Text as T import Simplex.Chat.Controller import Simplex.Chat.Core @@ -19,9 +17,8 @@ import Simplex.Chat.Messages import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Store -import Simplex.Chat.Types (Contact (..), IsContact (..), User (..)) +import Simplex.Chat.Types (Contact (..), ContactId, IsContact (..), User (..)) import Simplex.Messaging.Encoding.String (strEncode) -import Simplex.Messaging.Util (safeDecodeUtf8) import System.Exit (exitFailure) chatBotRepl :: String -> (Contact -> String -> IO String) -> User -> ChatController -> IO () @@ -32,49 +29,58 @@ chatBotRepl welcome answer _user cc = do case resp of CRContactConnected _ contact _ -> do contactConnected contact - void $ sendMsg contact welcome + void $ sendMessage cc contact welcome CRNewChatItem _ (AChatItem _ SMDRcv (DirectChat contact) ChatItem {content = mc@CIRcvMsgContent {}}) -> do let msg = T.unpack $ ciContentToText mc - void $ sendMsg contact =<< answer contact msg + void $ sendMessage cc contact =<< answer contact msg _ -> pure () where - sendMsg Contact {contactId} msg = sendChatCmd cc $ "/_send @" <> show contactId <> " text " <> msg contactConnected Contact {localDisplayName} = putStrLn $ T.unpack localDisplayName <> " connected" initializeBotAddress :: ChatController -> IO () -initializeBotAddress cc = do - sendChatCmd cc "/show_address" >>= \case +initializeBotAddress = initializeBotAddress' True + +initializeBotAddress' :: Bool -> ChatController -> IO () +initializeBotAddress' logAddress cc = do + sendChatCmd cc ShowMyAddress >>= \case CRUserContactLink _ UserContactLink {connReqContact} -> showBotAddress connReqContact CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do - putStrLn "No bot address, creating..." - sendChatCmd cc "/address" >>= \case + when logAddress $ putStrLn "No bot address, creating..." + sendChatCmd cc CreateMyAddress >>= \case CRUserContactLinkCreated _ uri -> showBotAddress uri _ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure where showBotAddress uri = do - putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) - void $ sendChatCmd cc "/auto_accept on" + when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) + void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {acceptIncognito = False, autoReply = Nothing} sendMessage :: ChatController -> Contact -> String -> IO () sendMessage cc ct = sendComposedMessage cc ct Nothing . textMsgContent +sendMessage' :: ChatController -> ContactId -> String -> IO () +sendMessage' cc ctId = sendComposedMessage' cc ctId Nothing . textMsgContent + sendComposedMessage :: ChatController -> Contact -> Maybe ChatItemId -> MsgContent -> IO () -sendComposedMessage cc ct quotedItemId msgContent = do - let cm = ComposedMessage {filePath = Nothing, quotedItemId, msgContent} - sendChatCmd cc ("/_send @" <> show (contactId' ct) <> " json " <> jsonEncode cm) >>= \case - CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to " <> contactInfo ct +sendComposedMessage cc = sendComposedMessage' cc . contactId' + +sendComposedMessage' :: ChatController -> ContactId -> Maybe ChatItemId -> MsgContent -> IO () +sendComposedMessage' cc ctId quotedItemId msgContent = do + let cm = ComposedMessage {fileSource = Nothing, quotedItemId, msgContent} + sendChatCmd cc (APISendMessage (ChatRef CTDirect ctId) False Nothing cm) >>= \case + CRNewChatItem {} -> printLog cc CLLInfo $ "sent message to contact ID " <> show ctId r -> putStrLn $ "unexpected send message response: " <> show r - where - jsonEncode = T.unpack . safeDecodeUtf8 . LB.toStrict . J.encode deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () deleteMessage cc ct chatItemId = do - let cmd = "/_delete item @" <> show (contactId' ct) <> " " <> show chatItemId <> " internal" + let cmd = APIDeleteChatItem (contactRef ct) chatItemId CIDMInternal sendChatCmd cc cmd >>= \case CRChatItemDeleted {} -> printLog cc CLLInfo $ "deleted message from " <> contactInfo ct r -> putStrLn $ "unexpected delete message response: " <> show r +contactRef :: Contact -> ChatRef +contactRef = ChatRef CTDirect . contactId' + textMsgContent :: String -> MsgContent textMsgContent = MCText . T.pack diff --git a/src/Simplex/Chat/Bot/KnownContacts.hs b/src/Simplex/Chat/Bot/KnownContacts.hs new file mode 100644 index 0000000000..c079b994a6 --- /dev/null +++ b/src/Simplex/Chat/Bot/KnownContacts.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Simplex.Chat.Bot.KnownContacts where + +import qualified Data.Attoparsec.ByteString.Char8 as A +import Data.Int (Int64) +import Data.Text (Text) +import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text as T +import Options.Applicative +import Simplex.Messaging.Parsers (parseAll) +import Simplex.Messaging.Util (safeDecodeUtf8) + +data KnownContact = KnownContact + { contactId :: Int64, + localDisplayName :: Text + } + deriving (Eq) + +knownContactNames :: [KnownContact] -> String +knownContactNames = T.unpack . T.intercalate ", " . map (("@" <>) . localDisplayName) + +parseKnownContacts :: ReadM [KnownContact] +parseKnownContacts = eitherReader $ parseAll knownContactsP . encodeUtf8 . T.pack + +knownContactsP :: A.Parser [KnownContact] +knownContactsP = contactP `A.sepBy1` A.char ',' + where + contactP = do + contactId <- A.decimal <* A.char ':' + localDisplayName <- safeDecodeUtf8 <$> A.takeTill (A.inClass ", ") + pure KnownContact {contactId, localDisplayName} diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 8091320717..2c829e4a95 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -22,8 +22,9 @@ import Control.Monad.Except import Control.Monad.IO.Unlift import Control.Monad.Reader import Crypto.Random (ChaChaDRG) -import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson (FromJSON (..), ToJSON (..), (.:), (.:?)) import qualified Data.Aeson as J +import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B @@ -48,21 +49,25 @@ import Simplex.Chat.Protocol import Simplex.Chat.Store (AutoAccept, StoreError, UserContactLink, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences -import Simplex.Messaging.Agent (AgentClient) +import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) import Simplex.Messaging.Agent.Client (AgentLocks, ProtocolTestFailure) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration) +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..)) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (simplexMQVersion) import Simplex.Messaging.Transport.Client (TransportHost) -import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors) +import Simplex.Messaging.Util (allFinally, catchAllErrors, tryAllErrors, (<$$>)) +import Simplex.Messaging.Version import System.IO (Handle) import System.Mem.Weak (Weak) import UnliftIO.STM @@ -71,7 +76,7 @@ versionNumber :: String versionNumber = showVersion SC.version versionString :: String -> String -versionString version = "SimpleX Chat v" <> version +versionString ver = "SimpleX Chat v" <> ver updateStr :: String updateStr = "To update run: curl -o- https://raw.githubusercontent.com/simplex-chat/simplex-chat/master/install.sh | bash" @@ -100,6 +105,7 @@ coreVersionInfo simplexmqCommit = data ChatConfig = ChatConfig { agentConfig :: AgentConfig, + chatVRange :: VersionRange, confirmMigrations :: MigrationConfirmation, defaultServers :: DefaultAgentServers, tbqSize :: Natural, @@ -170,13 +176,13 @@ data ChatController = ChatController outputQ :: TBQueue (Maybe CorrId, ChatResponse), notifyQ :: TBQueue Notification, sendNotification :: Notification -> IO (), + subscriptionMode :: TVar SubscriptionMode, chatLock :: Lock, sndFiles :: TVar (Map Int64 Handle), rcvFiles :: TVar (Map Int64 Handle), currentCalls :: TMap ContactId Call, config :: ChatConfig, filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps, - incognitoMode :: TVar Bool, expireCIThreads :: TMap UserId (Maybe (Async ())), expireCIFlags :: TMap UserId Bool, cleanupManagerAsync :: TVar (Maybe (Async ())), @@ -187,7 +193,7 @@ data ChatController = ChatController logFilePath :: Maybe FilePath } -data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSMarkdown | HSMessages | HSSettings | HSDatabase +data HelpSection = HSMain | HSFiles | HSGroups | HSContacts | HSMyAddress | HSIncognito | HSMarkdown | HSMessages | HSSettings | HSDatabase deriving (Show, Generic) instance ToJSON HelpSection where @@ -203,6 +209,8 @@ data ChatCommand | SetAllContactReceipts Bool | APISetUserContactReceipts UserId UserMsgReceiptSettings | SetUserContactReceipts UserMsgReceiptSettings + | APISetUserGroupReceipts UserId UserMsgReceiptSettings + | SetUserGroupReceipts UserMsgReceiptSettings | APIHideUser UserId UserPwd | APIUnhideUser UserId UserPwd | APIMuteUser UserId @@ -221,7 +229,6 @@ data ChatCommand | SetTempFolder FilePath | SetFilesFolder FilePath | APISetXFTPConfig (Maybe XFTPFileConfig) - | SetIncognito Bool | APIExportArchive ArchiveConfig | ExportArchive | APIImportArchive ArchiveConfig @@ -229,6 +236,7 @@ data ChatCommand | APIStorageEncryption DBEncryptionConfig | ExecChatStoreSQL Text | ExecAgentStoreSQL Text + | SlowSQLQueries | APIGetChats {userId :: UserId, pendingConnections :: Bool} | APIGetChat ChatRef ChatPagination (Maybe String) | APIGetChatItems ChatPagination (Maybe String) @@ -242,7 +250,7 @@ data ChatCommand | APIChatUnread ChatRef Bool | APIDeleteChat ChatRef | APIClearChat ChatRef - | APIAcceptContact Int64 + | APIAcceptContact IncognitoEnabled Int64 | APIRejectContact Int64 | APISendCallInvitation ContactId CallType | SendCallInvitation ContactName CallType @@ -274,6 +282,8 @@ data ChatCommand | APIGroupLinkMemberRole GroupId GroupMemberRole | APIDeleteGroupLink GroupId | APIGetGroupLink GroupId + | APICreateMemberContact GroupId GroupMemberId + | APISendMemberContactInvitation {contactId :: ContactId, msgContent_ :: Maybe MsgContent} | APIGetUserProtoServers UserId AProtocolType | GetUserProtoServers AProtocolType | APISetUserProtoServers UserId AProtoServersConfig @@ -289,6 +299,7 @@ data ChatCommand | ReconnectAllServers | APISetChatSettings ChatRef ChatSettings | APIContactInfo ContactId + | APIGroupInfo GroupId | APIGroupMemberInfo GroupId GroupMemberId | APISwitchContact ContactId | APISwitchGroupMember GroupId GroupMemberId @@ -305,6 +316,7 @@ data ChatCommand | SetShowMessages ChatName Bool | SetSendReceipts ChatName (Maybe Bool) | ContactInfo ContactName + | ShowGroupInfo GroupName | GroupMemberInfo GroupName ContactName | SwitchContact ContactName | SwitchGroupMember GroupName ContactName @@ -320,11 +332,12 @@ data ChatCommand | EnableGroupMember GroupName ContactName | ChatHelp HelpSection | Welcome - | APIAddContact UserId - | AddContact - | APIConnect UserId (Maybe AConnectionRequestUri) - | Connect (Maybe AConnectionRequestUri) - | ConnectSimplex -- UserId (not used in UI) + | APIAddContact UserId IncognitoEnabled + | AddContact IncognitoEnabled + | APISetConnectionIncognito Int64 IncognitoEnabled + | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) + | Connect IncognitoEnabled (Maybe AConnectionRequestUri) + | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | DeleteContact ContactName | ClearContact ContactName | APIListContacts UserId @@ -339,9 +352,10 @@ data ChatCommand | SetProfileAddress Bool | APIAddressAutoAccept UserId (Maybe AutoAccept) | AddressAutoAccept (Maybe AutoAccept) - | AcceptContact ContactName + | AcceptContact IncognitoEnabled ContactName | RejectContact ContactName | SendMessage ChatName Text + | SendMemberContactMessage GroupName ContactName Text | SendLiveMessage ChatName Text | SendMessageQuote {contactName :: ContactName, msgDir :: AMsgDirection, quotedMsg :: Text, message :: Text} | SendMessageBroadcast Text -- UserId (not used in UI) @@ -360,10 +374,12 @@ data ChatCommand | DeleteGroup GroupName | ClearGroup GroupName | ListMembers GroupName - | ListGroups -- UserId (not used in UI) + | APIListGroups UserId (Maybe ContactId) (Maybe String) + | ListGroups (Maybe ContactName) (Maybe String) | UpdateGroupNames GroupName GroupProfile | ShowGroupProfile GroupName | UpdateGroupDescription GroupName (Maybe Text) + | ShowGroupDescription GroupName | CreateGroupLink GroupName GroupMemberRole | GroupLinkMemberRole GroupName GroupMemberRole | DeleteGroupLink GroupName @@ -380,8 +396,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive FileTransferId + | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) @@ -399,6 +415,8 @@ data ChatCommand | DebugLocks | GetAgentStats | ResetAgentStats + | GetAgentSubs + | GetAgentSubsDetails deriving (Show) data ChatResponse @@ -420,6 +438,7 @@ data ChatResponse | CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64} | CRNetworkConfig {networkConfig :: NetworkConfig} | CRContactInfo {user :: User, contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile} + | CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary} | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} | CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} @@ -457,7 +476,7 @@ data ChatResponse | CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest} | CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact} | CRUserDeletedMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} - | CRGroupsList {user :: User, groups :: [GroupInfo]} + | CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]} | CRSentGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, member :: GroupMember} | CRFileTransferStatus User (FileTransfer, [Integer]) -- TODO refactor this type to FileTransferStatus | CRFileTransferStatusXFTP User AChatItem @@ -465,7 +484,8 @@ data ChatResponse | CRUserProfileNoChange {user :: User} | CRUserPrivacy {user :: User, updatedUser :: User} | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} - | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation} + | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} + | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} | CRSentConfirmation {user :: User} | CRSentInvitation {user :: User, customUserProfile :: Maybe Profile} | CRContactUpdated {user :: User, fromContact :: Contact, toContact :: Contact} @@ -489,7 +509,7 @@ data ChatResponse | CRRcvFileComplete {user :: User, chatItem :: AChatItem} | CRRcvFileCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} - | CRRcvFileError {user :: User, chatItem :: AChatItem} + | CRRcvFileError {user :: User, chatItem :: AChatItem, agentError :: AgentErrorType} | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileRcvCancelled {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} @@ -499,7 +519,7 @@ data ChatResponse | CRSndFileCompleteXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} | CRSndFileCancelledXFTP {user :: User, chatItem :: AChatItem, fileTransferMeta :: FileTransferMeta} | CRSndFileError {user :: User, chatItem :: AChatItem} - | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, successes :: Int, failures :: Int} + | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary} | CRUserProfileImage {user :: User, profile :: Profile} | CRContactAliasUpdated {user :: User, toContact :: Contact} | CRConnectionAliasUpdated {user :: User, toConnection :: PendingContactConnection} @@ -516,7 +536,7 @@ data ChatResponse | CRHostConnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRHostDisconnected {protocol :: AProtocolType, transportHost :: TransportHost} | CRGroupInvitation {user :: User, groupInfo :: GroupInfo} - | CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, memberRole :: GroupMemberRole} + | CRReceivedGroupInvitation {user :: User, groupInfo :: GroupInfo, contact :: Contact, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} | CRUserJoinedGroup {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember} | CRJoinedGroupMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRJoinedGroupMemberConnecting {user :: User, groupInfo :: GroupInfo, hostMember :: GroupMember, member :: GroupMember} @@ -531,10 +551,16 @@ data ChatResponse | CRGroupDeleted {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} | CRGroupProfile {user :: User, groupInfo :: GroupInfo} + | CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} | CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} | CRAcceptingGroupJoinRequest {user :: User, groupInfo :: GroupInfo, contact :: Contact} + | CRNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI + | CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | CRNewMemberContactReceivedInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} + | CRMemberContactConnected {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember} | CRMemberSubError {user :: User, groupInfo :: GroupInfo, member :: GroupMember, chatError :: ChatError} | CRMemberSubSummary {user :: User, memberSubscriptions :: [MemberSubStatus]} | CRGroupSubscribed {user :: User, groupInfo :: GroupInfo} @@ -555,8 +581,11 @@ data ChatResponse | CRNewContactConnection {user :: User, connection :: PendingContactConnection} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRSQLResult {rows :: [Text]} + | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRDebugLocks {chatLockName :: Maybe String, agentLocks :: AgentLocks} | CRAgentStats {agentStats :: [[String]]} + | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} + | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} | CRConnectionDisabled {connectionEntity :: ConnectionEntity} | CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType} | CRAgentConnDeleted {agentConnId :: AgentConnId} @@ -697,12 +726,35 @@ instance ToJSON PendingSubStatus where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} +data UserProfileUpdateSummary = UserProfileUpdateSummary + { notChanged :: Int, + updateSuccesses :: Int, + updateFailures :: Int, + changedContacts :: [Contact] + } + deriving (Show, Generic) + +instance ToJSON UserProfileUpdateSummary where toEncoding = J.genericToEncoding J.defaultOptions + data ComposedMessage = ComposedMessage - { filePath :: Maybe FilePath, + { fileSource :: Maybe CryptoFile, quotedItemId :: Maybe ChatItemId, msgContent :: MsgContent } - deriving (Show, Generic, FromJSON) + deriving (Show, Generic) + +-- This instance is needed for backward compatibility, can be removed in v6.0 +instance FromJSON ComposedMessage where + parseJSON (J.Object v) = do + fileSource <- + (v .:? "fileSource") >>= \case + Nothing -> CF.plain <$$> (v .:? "filePath") + f -> pure f + quotedItemId <- v .:? "quotedItemId" + msgContent <- v .: "msgContent" + pure ComposedMessage {fileSource, quotedItemId, msgContent} + parseJSON invalid = + JT.prependFailure "bad ComposedMessage, " (JT.typeMismatch "Object" invalid) instance ToJSON ComposedMessage where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} @@ -793,6 +845,14 @@ data SendFileMode | SendFileXFTP deriving (Show, Generic) +data SlowSQLQuery = SlowSQLQuery + { query :: Text, + queryStats :: SlowQueryStats + } + deriving (Show, Generic) + +instance ToJSON SlowSQLQuery where toEncoding = J.genericToEncoding J.defaultOptions + data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType, connectionEntity_ :: Maybe ConnectionEntity} @@ -825,6 +885,7 @@ data ChatErrorType | CEChatStoreChanged | CEInvalidConnReq | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} + | CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)} | CEContactNotReady {contact :: Contact} | CEContactDisabled {contact :: Contact} | CEConnectionDisabled {connection :: Connection} @@ -874,6 +935,8 @@ data ChatErrorType | CEServerProtocol {serverProtocol :: AProtocolType} | CEAgentCommandError {message :: String} | CEInvalidFileDescription {message :: String} + | CEConnectionIncognitoChangeProhibited + | CEPeerChatVRangeIncompatible | CEInternalError {message :: String} | CEException {message :: String} deriving (Show, Exception, Generic) @@ -908,6 +971,14 @@ type ChatMonad' m = (MonadUnliftIO m, MonadReader ChatController m) type ChatMonad m = (ChatMonad' m, MonadError ChatError m) +chatReadVar :: ChatMonad' m => (ChatController -> TVar a) -> m a +chatReadVar f = asks f >>= readTVarIO +{-# INLINE chatReadVar #-} + +chatWriteVar :: ChatMonad' m => (ChatController -> TVar a) -> a -> m () +chatWriteVar f value = asks f >>= atomically . (`writeTVar` value) +{-# INLINE chatWriteVar #-} + tryChatError :: ChatMonad m => m a -> m (Either ChatError a) tryChatError = tryAllErrors mkChatError {-# INLINE tryChatError #-} diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index e23dbc5a96..4af161ab41 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -15,7 +15,7 @@ import System.Exit (exitFailure) import UnliftIO.Async simplexChatCore :: ChatConfig -> ChatOpts -> Maybe (Notification -> IO ()) -> (User -> ChatController -> IO ()) -> IO () -simplexChatCore cfg@ChatConfig {confirmMigrations} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} sendToast chat = +simplexChatCore cfg@ChatConfig {confirmMigrations, testView} opts@ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, dbKey, logAgent}} sendToast chat = case logAgent of Just level -> do setLogLevel level @@ -27,7 +27,7 @@ simplexChatCore cfg@ChatConfig {confirmMigrations} opts@ChatOpts {coreOptions = putStrLn $ "Error opening database: " <> show e exitFailure run db@ChatDatabase {chatStore} = do - u <- getCreateActiveUser chatStore + u <- getCreateActiveUser chatStore testView cc <- newChatController db (Just u) cfg opts sendToast runSimplexChat opts u cc chat @@ -39,5 +39,8 @@ runSimplexChat ChatOpts {maintenance} u cc chat a2 <- async $ chat u cc waitEither_ a1 a2 -sendChatCmd :: ChatController -> String -> IO ChatResponse -sendChatCmd cc s = runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc +sendChatCmdStr :: ChatController -> String -> IO ChatResponse +sendChatCmdStr cc s = runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc + +sendChatCmd :: ChatController -> ChatCommand -> IO ChatResponse +sendChatCmd cc cmd = runReaderT (execChatCommand' cmd) cc diff --git a/src/Simplex/Chat/Help.hs b/src/Simplex/Chat/Help.hs index c83e81a9ef..c2a5720633 100644 --- a/src/Simplex/Chat/Help.hs +++ b/src/Simplex/Chat/Help.hs @@ -8,6 +8,7 @@ module Simplex.Chat.Help groupsHelpInfo, contactsHelpInfo, myAddressHelpInfo, + incognitoHelpInfo, messagesHelpInfo, markdownInfo, settingsInfo, @@ -48,7 +49,7 @@ chatWelcome user = "Welcome " <> green userName <> "!", "Thank you for installing SimpleX Chat!", "", - "Connect to SimpleX Chat lead developer for any questions - just type " <> highlight "/simplex", + "Connect to SimpleX Chat developers for any questions - just type " <> highlight "/simplex", "", "Follow our updates:", "> Reddit: https://www.reddit.com/r/SimpleXChat/", @@ -213,6 +214,26 @@ myAddressHelpInfo = "The commands may be abbreviated: " <> listHighlight ["/ad", "/da", "/sa", "/ac", "/rc"] ] +incognitoHelpInfo :: [StyledString] +incognitoHelpInfo = + map + styleMarkdown + [ markdown (colored Red) "/incognito" <> " command is deprecated, use commands below instead.", + "", + "Incognito mode protects the privacy of your main profile — you can choose to create a new random profile for each new contact.", + "It allows having many anonymous connections without any shared data between them in a single chat profile.", + "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to.", + "", + green "Incognito commands:", + indent <> highlight "/connect incognito " <> " - create new invitation link using incognito profile", + indent <> highlight "/connect incognito " <> " - accept invitation using incognito profile", + indent <> highlight "/accept incognito " <> " - accept contact request using incognito profile", + indent <> highlight "/simplex incognito " <> " - connect to SimpleX Chat developers using incognito profile", + "", + "The commands may be abbreviated: " <> listHighlight ["/c i", "/c i ", "/ac i "], + "To find the profile used for an incognito connection, use " <> highlight "/info " <> "." + ] + messagesHelpInfo :: [StyledString] messagesHelpInfo = map @@ -269,7 +290,6 @@ settingsInfo = map styleMarkdown [ green "Chat settings:", - indent <> highlight "/incognito on/off " <> " - enable/disable incognito mode", indent <> highlight "/network " <> " - show / set network access options", indent <> highlight "/smp " <> " - show / set configured SMP servers", indent <> highlight "/xftp " <> " - show / set configured XFTP servers", @@ -285,12 +305,12 @@ databaseHelpInfo :: [StyledString] databaseHelpInfo = map styleMarkdown - [ green "Database export:", - indent <> highlight "/db export " <> " - create database export file that can be imported in mobile apps", - indent <> highlight "/files_folder " <> " - set files folder path to include app files in the exported archive", - "", - green "Database encryption:", - indent <> highlight "/db encrypt " <> " - encrypt chat database with key/passphrase", - indent <> highlight "/db key " <> " - change the key of the encrypted app database", - indent <> highlight "/db decrypt " <> " - decrypt chat database" - ] + [ green "Database export:", + indent <> highlight "/db export " <> " - create database export file that can be imported in mobile apps", + indent <> highlight "/files_folder " <> " - set files folder path to include app files in the exported archive", + "", + green "Database encryption:", + indent <> highlight "/db encrypt " <> " - encrypt chat database with key/passphrase", + indent <> highlight "/db key " <> " - change the key of the encrypted app database", + indent <> highlight "/db decrypt " <> " - decrypt chat database" + ] diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 1464f6247b..45e5f9ff74 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -21,7 +21,7 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy.Char8 as LB import Data.Int (Int64) -import Data.Maybe (isJust, isNothing) +import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -37,6 +37,8 @@ import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) +import Simplex.Messaging.Crypto.File (CryptoFile (..)) +import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, parseAll, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) @@ -459,7 +461,7 @@ data CIFile (d :: MsgDirection) = CIFile { fileId :: Int64, fileName :: String, fileSize :: Integer, - filePath :: Maybe FilePath, -- local file path + fileSource :: Maybe CryptoFile, -- local file path with optional key and nonce fileStatus :: CIFileStatus d, fileProtocol :: FileProtocol } @@ -631,16 +633,26 @@ data CIFileInfo = CIFileInfo } deriving (Show) +mkCIFileInfo :: MsgDirectionI d => CIFile d -> CIFileInfo +mkCIFileInfo CIFile {fileId, fileStatus, fileSource} = + CIFileInfo + { fileId, + fileStatus = Just $ AFS msgDirection fileStatus, + filePath = CF.filePath <$> fileSource + } + data CIStatus (d :: MsgDirection) where CISSndNew :: CIStatus 'MDSnd - CISSndSent :: CIStatus 'MDSnd - CISSndRcvd :: MsgReceiptStatus -> CIStatus 'MDSnd + CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd + CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd CISSndErrorAuth :: CIStatus 'MDSnd CISSndError :: String -> CIStatus 'MDSnd CISRcvNew :: CIStatus 'MDRcv CISRcvRead :: CIStatus 'MDRcv CISInvalid :: Text -> CIStatus 'MDSnd +deriving instance Eq (CIStatus d) + deriving instance Show (CIStatus d) instance ToJSON (CIStatus d) where @@ -649,6 +661,8 @@ instance ToJSON (CIStatus d) where instance MsgDirectionI d => ToField (CIStatus d) where toField = toField . decodeLatin1 . strEncode +instance (Typeable d, MsgDirectionI d) => FromField (CIStatus d) where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 + instance FromField ACIStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 data ACIStatus = forall d. MsgDirectionI d => ACIStatus (SMsgDirection d) (CIStatus d) @@ -658,8 +672,8 @@ deriving instance Show ACIStatus instance MsgDirectionI d => StrEncoding (CIStatus d) where strEncode = \case CISSndNew -> "snd_new" - CISSndSent -> "snd_sent" - CISSndRcvd status -> "snd_rcvd " <> strEncode status + CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress + CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress CISSndErrorAuth -> "snd_error_auth" CISSndError e -> "snd_error " <> encodeUtf8 (T.pack e) CISRcvNew -> "rcv_new" @@ -676,8 +690,8 @@ instance StrEncoding ACIStatus where statusP = A.takeTill (== ' ') >>= \case "snd_new" -> pure $ ACIStatus SMDSnd CISSndNew - "snd_sent" -> pure $ ACIStatus SMDSnd CISSndSent - "snd_rcvd" -> ACIStatus SMDSnd . CISSndRcvd <$> (A.space *> strP) + "snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete) + "snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete)) "snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth "snd_error" -> ACIStatus SMDSnd . CISSndError . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) "rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew @@ -686,8 +700,8 @@ instance StrEncoding ACIStatus where data JSONCIStatus = JCISSndNew - | JCISSndSent - | JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus} + | JCISSndSent {sndProgress :: SndCIStatusProgress} + | JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress} | JCISSndErrorAuth | JCISSndError {agentError :: String} | JCISRcvNew @@ -702,8 +716,8 @@ instance ToJSON JSONCIStatus where jsonCIStatus :: CIStatus d -> JSONCIStatus jsonCIStatus = \case CISSndNew -> JCISSndNew - CISSndSent -> JCISSndSent - CISSndRcvd ok -> JCISSndRcvd ok + CISSndSent sndProgress -> JCISSndSent sndProgress + CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress CISSndErrorAuth -> JCISSndErrorAuth CISSndError e -> JCISSndError e CISRcvNew -> JCISRcvNew @@ -720,6 +734,40 @@ ciCreateStatus content = case msgDirection @d of SMDSnd -> ciStatusNew SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead +membersGroupItemStatus :: [(CIStatus 'MDSnd, Int)] -> CIStatus 'MDSnd +membersGroupItemStatus memStatusCounts + | rcvdOk == total = CISSndRcvd MROk SSPComplete + | rcvdOk + rcvdBad == total = CISSndRcvd MRBadMsgHash SSPComplete + | rcvdBad > 0 = CISSndRcvd MRBadMsgHash SSPPartial + | rcvdOk > 0 = CISSndRcvd MROk SSPPartial + | sent == total = CISSndSent SSPComplete + | sent > 0 = CISSndSent SSPPartial + | otherwise = CISSndNew + where + total = sum $ map snd memStatusCounts + rcvdOk = fromMaybe 0 $ lookup (CISSndRcvd MROk SSPComplete) memStatusCounts + rcvdBad = fromMaybe 0 $ lookup (CISSndRcvd MRBadMsgHash SSPComplete) memStatusCounts + sent = fromMaybe 0 $ lookup (CISSndSent SSPComplete) memStatusCounts + +data SndCIStatusProgress + = SSPPartial + | SSPComplete + deriving (Eq, Show, Generic) + +instance ToJSON SndCIStatusProgress where + toJSON = J.genericToJSON . enumJSON $ dropPrefix "SSP" + toEncoding = J.genericToEncoding . enumJSON $ dropPrefix "SSP" + +instance StrEncoding SndCIStatusProgress where + strEncode = \case + SSPPartial -> "partial" + SSPComplete -> "complete" + strP = + A.takeWhile1 (/= ' ') >>= \case + "partial" -> pure SSPPartial + "complete" -> pure SSPComplete + _ -> fail "bad SndCIStatusProgress" + type ChatItemId = Int64 type ChatItemTs = UTCTime @@ -904,7 +952,8 @@ itemDeletedTs = \case CIModerated ts _ -> ts data ChatItemInfo = ChatItemInfo - { itemVersions :: [ChatItemVersion] + { itemVersions :: [ChatItemVersion], + memberDeliveryStatuses :: Maybe [MemberDeliveryStatus] } deriving (Eq, Show, Generic) @@ -934,6 +983,14 @@ mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content createdAt = createdAt } +data MemberDeliveryStatus = MemberDeliveryStatus + { groupMemberId :: GroupMemberId, + memberDeliveryStatus :: CIStatus 'MDSnd + } + deriving (Eq, Show, Generic) + +instance ToJSON MemberDeliveryStatus where toEncoding = J.genericToEncoding J.defaultOptions + data CIModeration = CIModeration { moderationId :: Int64, moderatorMember :: GroupMember, diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 725cf74cf9..df22c2684c 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -50,7 +50,7 @@ instance FromField AMsgDirection where fromField = fromIntField_ $ fmap fromMsgD instance ToField MsgDirection where toField = toField . msgDirectionInt -fromIntField_ :: (Typeable a) => (Int64 -> Maybe a) -> Field -> Ok a +fromIntField_ :: Typeable a => (Int64 -> Maybe a) -> Field -> Ok a fromIntField_ fromInt = \case f@(Field (SQLInteger i) _) -> case fromInt i of @@ -190,6 +190,7 @@ ciRequiresAttention content = case msgDirection @d of RGEGroupDeleted -> True RGEGroupUpdated _ -> False RGEInvitedViaGroupLink -> False + RGEMemberCreatedContact -> False CIRcvConnEvent _ -> True CIRcvChatFeature {} -> False CIRcvChatPreference {} -> False @@ -213,6 +214,7 @@ data RcvGroupEvent -- but being RcvGroupEvent allows them to be assigned to the respective member (and so enable "send direct message") -- and be created as unread without adding / working around new status for sent items | RGEInvitedViaGroupLink -- CRSentGroupInvitationViaLink + | RGEMemberCreatedContact -- CRNewMemberContactReceivedInv deriving (Show, Generic) instance FromJSON RcvGroupEvent where @@ -378,6 +380,7 @@ rcvGroupEventToText = \case RGEGroupDeleted -> "deleted group" RGEGroupUpdated _ -> "group profile updated" RGEInvitedViaGroupLink -> "invited via your group link" + RGEMemberCreatedContact -> "started direct connection with you" sndGroupEventToText :: SndGroupEvent -> Text sndGroupEventToText = \case diff --git a/src/Simplex/Chat/Migrations/M20230721_group_snd_item_statuses.hs b/src/Simplex/Chat/Migrations/M20230721_group_snd_item_statuses.hs new file mode 100644 index 0000000000..8453da88f5 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230721_group_snd_item_statuses.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230721_group_snd_item_statuses where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230721_group_snd_item_statuses :: Query +m20230721_group_snd_item_statuses = + [sql| +CREATE TABLE group_snd_item_statuses ( + group_snd_item_status_id INTEGER PRIMARY KEY, + chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, + group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + group_snd_item_status TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); + +CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses(chat_item_id); +CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses(group_member_id); + +UPDATE users SET send_rcpts_small_groups = 1 WHERE send_rcpts_contacts = 1; +|] + +down_m20230721_group_snd_item_statuses :: Query +down_m20230721_group_snd_item_statuses = + [sql| +DROP INDEX idx_group_snd_item_statuses_group_member_id; +DROP INDEX idx_group_snd_item_statuses_chat_item_id; + +DROP TABLE group_snd_item_statuses; +|] diff --git a/src/Simplex/Chat/Migrations/M20230814_indexes.hs b/src/Simplex/Chat/Migrations/M20230814_indexes.hs new file mode 100644 index 0000000000..a7419037ef --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230814_indexes.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230814_indexes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230814_indexes :: Query +m20230814_indexes = + [sql| +CREATE INDEX idx_chat_items_user_id_item_status ON chat_items(user_id, item_status); +|] + +down_m20230814_indexes :: Query +down_m20230814_indexes = + [sql| +DROP INDEX idx_chat_items_user_id_item_status; +|] diff --git a/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs b/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs new file mode 100644 index 0000000000..2e659cac84 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230827_file_encryption.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230827_file_encryption where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230827_file_encryption :: Query +m20230827_file_encryption = + [sql| +ALTER TABLE files ADD COLUMN file_crypto_key BLOB; +ALTER TABLE files ADD COLUMN file_crypto_nonce BLOB; +|] + +down_m20230827_file_encryption :: Query +down_m20230827_file_encryption = + [sql| +ALTER TABLE files DROP COLUMN file_crypto_key; +ALTER TABLE files DROP COLUMN file_crypto_nonce; +|] diff --git a/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs b/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs new file mode 100644 index 0000000000..2588553a92 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230829_connections_chat_vrange.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230829_connections_chat_vrange where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230829_connections_chat_vrange :: Query +m20230829_connections_chat_vrange = + [sql| +ALTER TABLE connections ADD COLUMN peer_chat_min_version INTEGER NOT NULL DEFAULT 1; +ALTER TABLE connections ADD COLUMN peer_chat_max_version INTEGER NOT NULL DEFAULT 1; + +ALTER TABLE contact_requests ADD COLUMN peer_chat_min_version INTEGER NOT NULL DEFAULT 1; +ALTER TABLE contact_requests ADD COLUMN peer_chat_max_version INTEGER NOT NULL DEFAULT 1; +|] + +down_m20230829_connections_chat_vrange :: Query +down_m20230829_connections_chat_vrange = + [sql| +ALTER TABLE contact_requests DROP COLUMN peer_chat_max_version; +ALTER TABLE contact_requests DROP COLUMN peer_chat_min_version; + +ALTER TABLE connections DROP COLUMN peer_chat_max_version; +ALTER TABLE connections DROP COLUMN peer_chat_min_version; +|] diff --git a/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs b/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs new file mode 100644 index 0000000000..48ad8dbf86 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230903_connections_to_subscribe.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230903_connections_to_subscribe where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230903_connections_to_subscribe :: Query +m20230903_connections_to_subscribe = + [sql| +ALTER TABLE connections ADD COLUMN to_subscribe INTEGER DEFAULT 0 NOT NULL; +CREATE INDEX idx_connections_to_subscribe ON connections(to_subscribe); +|] + +down_m20230903_connections_to_subscribe :: Query +down_m20230903_connections_to_subscribe = + [sql| +DROP INDEX idx_connections_to_subscribe; +ALTER TABLE connections DROP COLUMN to_subscribe; +|] diff --git a/src/Simplex/Chat/Migrations/M20230913_member_contacts.hs b/src/Simplex/Chat/Migrations/M20230913_member_contacts.hs new file mode 100644 index 0000000000..b116373518 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230913_member_contacts.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230913_member_contacts where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230913_member_contacts :: Query +m20230913_member_contacts = + [sql| +ALTER TABLE contacts ADD COLUMN contact_group_member_id INTEGER + REFERENCES group_members(group_member_id) ON DELETE SET NULL; + +CREATE INDEX idx_contacts_contact_group_member_id ON contacts(contact_group_member_id); + +ALTER TABLE contacts ADD COLUMN contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0; +|] + +down_m20230913_member_contacts :: Query +down_m20230913_member_contacts = + [sql| +ALTER TABLE contacts DROP COLUMN contact_grp_inv_sent; + +DROP INDEX idx_contacts_contact_group_member_id; + +ALTER TABLE contacts DROP COLUMN contact_group_member_id; +|] diff --git a/src/Simplex/Chat/Migrations/M20230914_member_probes.hs b/src/Simplex/Chat/Migrations/M20230914_member_probes.hs new file mode 100644 index 0000000000..8772b6cdad --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20230914_member_probes.hs @@ -0,0 +1,169 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20230914_member_probes where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20230914_member_probes :: Query +m20230914_member_probes = + [sql| +CREATE TABLE new__sent_probes( + sent_probe_id INTEGER PRIMARY KEY, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + probe BLOB NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(user_id, probe) +); + +CREATE TABLE new__sent_probe_hashes( + sent_probe_hash_id INTEGER PRIMARY KEY, + sent_probe_id INTEGER NOT NULL REFERENCES new__sent_probes ON DELETE CASCADE, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); + +CREATE TABLE new__received_probes( + received_probe_id INTEGER PRIMARY KEY, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + probe BLOB, + probe_hash BLOB NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); + +INSERT INTO new__sent_probes + (sent_probe_id, contact_id, probe, user_id, created_at, updated_at) +SELECT + sent_probe_id, contact_id, probe, user_id, created_at, updated_at + FROM sent_probes; + +INSERT INTO new__sent_probe_hashes + (sent_probe_hash_id, sent_probe_id, contact_id, user_id, created_at, updated_at) +SELECT + sent_probe_hash_id, sent_probe_id, contact_id, user_id, created_at, updated_at + FROM sent_probe_hashes; + +INSERT INTO new__received_probes + (received_probe_id, contact_id, probe, probe_hash, user_id, created_at, updated_at) +SELECT + received_probe_id, contact_id, probe, probe_hash, user_id, created_at, updated_at + FROM received_probes; + +DROP INDEX idx_sent_probe_hashes_user_id; +DROP INDEX idx_sent_probe_hashes_contact_id; +DROP INDEX idx_received_probes_user_id; +DROP INDEX idx_received_probes_contact_id; + +DROP TABLE sent_probes; +DROP TABLE sent_probe_hashes; +DROP TABLE received_probes; + +ALTER TABLE new__sent_probes RENAME TO sent_probes; +ALTER TABLE new__sent_probe_hashes RENAME TO sent_probe_hashes; +ALTER TABLE new__received_probes RENAME TO received_probes; + +CREATE INDEX idx_sent_probes_user_id ON sent_probes(user_id); +CREATE INDEX idx_sent_probes_contact_id ON sent_probes(contact_id); +CREATE INDEX idx_sent_probes_group_member_id ON sent_probes(group_member_id); + +CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id); +CREATE INDEX idx_sent_probe_hashes_sent_probe_id ON sent_probe_hashes(sent_probe_id); +CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id); +CREATE INDEX idx_sent_probe_hashes_group_member_id ON sent_probe_hashes(group_member_id); + +CREATE INDEX idx_received_probes_user_id ON received_probes(user_id); +CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id); +CREATE INDEX idx_received_probes_probe ON received_probes(probe); +CREATE INDEX idx_received_probes_probe_hash ON received_probes(probe_hash); +|] + +down_m20230914_member_probes :: Query +down_m20230914_member_probes = + [sql| +CREATE TABLE old__sent_probes( + sent_probe_id INTEGER PRIMARY KEY, + contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, + probe BLOB NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(user_id, probe) +); + +CREATE TABLE old__sent_probe_hashes( + sent_probe_hash_id INTEGER PRIMARY KEY, + sent_probe_id INTEGER NOT NULL REFERENCES old__sent_probes ON DELETE CASCADE, + contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); + +CREATE TABLE old__received_probes( + received_probe_id INTEGER PRIMARY KEY, + contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, + probe BLOB, + probe_hash BLOB NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); + +DELETE FROM sent_probes WHERE contact_id IS NULL; +DELETE FROM sent_probe_hashes WHERE contact_id IS NULL; +DELETE FROM received_probes WHERE contact_id IS NULL; + +INSERT INTO old__sent_probes + (sent_probe_id, contact_id, probe, user_id, created_at, updated_at) +SELECT + sent_probe_id, contact_id, probe, user_id, created_at, updated_at + FROM sent_probes; + +INSERT INTO old__sent_probe_hashes + (sent_probe_hash_id, sent_probe_id, contact_id, user_id, created_at, updated_at) +SELECT + sent_probe_hash_id, sent_probe_id, contact_id, user_id, created_at, updated_at + FROM sent_probe_hashes; + +INSERT INTO old__received_probes + (received_probe_id, contact_id, probe, probe_hash, user_id, created_at, updated_at) +SELECT + received_probe_id, contact_id, probe, probe_hash, user_id, created_at, updated_at + FROM received_probes; + +DROP INDEX idx_sent_probes_user_id; +DROP INDEX idx_sent_probes_contact_id; +DROP INDEX idx_sent_probes_group_member_id; + +DROP INDEX idx_sent_probe_hashes_user_id; +DROP INDEX idx_sent_probe_hashes_sent_probe_id; +DROP INDEX idx_sent_probe_hashes_contact_id; +DROP INDEX idx_sent_probe_hashes_group_member_id; + +DROP INDEX idx_received_probes_user_id; +DROP INDEX idx_received_probes_contact_id; +DROP INDEX idx_received_probes_probe; +DROP INDEX idx_received_probes_probe_hash; + +DROP TABLE sent_probes; +DROP TABLE sent_probe_hashes; +DROP TABLE received_probes; + +ALTER TABLE old__sent_probes RENAME TO sent_probes; +ALTER TABLE old__sent_probe_hashes RENAME TO sent_probe_hashes; +ALTER TABLE old__received_probes RENAME TO received_probes; + +CREATE INDEX idx_received_probes_user_id ON received_probes(user_id); +CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id); +CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id); +CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id); +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index 893d00d345..141247e590 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -68,6 +68,9 @@ CREATE TABLE contacts( deleted INTEGER NOT NULL DEFAULT 0, favorite INTEGER NOT NULL DEFAULT 0, send_rcpts INTEGER, + contact_group_member_id INTEGER + REFERENCES group_members(group_member_id) ON DELETE SET NULL, + contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -75,34 +78,6 @@ CREATE TABLE contacts( UNIQUE(user_id, local_display_name), UNIQUE(user_id, contact_profile_id) ); -CREATE TABLE sent_probes( - sent_probe_id INTEGER PRIMARY KEY, - contact_id INTEGER NOT NULL UNIQUE REFERENCES contacts ON DELETE CASCADE, - probe BLOB NOT NULL, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - created_at TEXT CHECK(created_at NOT NULL), - updated_at TEXT CHECK(updated_at NOT NULL), - UNIQUE(user_id, probe) -); -CREATE TABLE sent_probe_hashes( - sent_probe_hash_id INTEGER PRIMARY KEY, - sent_probe_id INTEGER NOT NULL REFERENCES sent_probes ON DELETE CASCADE, - contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, - created_at TEXT CHECK(created_at NOT NULL), - updated_at TEXT CHECK(updated_at NOT NULL), - UNIQUE(sent_probe_id, contact_id) -); -CREATE TABLE received_probes( - received_probe_id INTEGER PRIMARY KEY, - contact_id INTEGER NOT NULL REFERENCES contacts ON DELETE CASCADE, - probe BLOB, - probe_hash BLOB NOT NULL, - user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE - , - created_at TEXT CHECK(created_at NOT NULL), - updated_at TEXT CHECK(updated_at NOT NULL) -); CREATE TABLE known_servers( server_id INTEGER PRIMARY KEY, host TEXT NOT NULL, @@ -204,7 +179,9 @@ CREATE TABLE files( agent_snd_file_id BLOB NULL, private_snd_file_descr TEXT NULL, agent_snd_file_deleted INTEGER DEFAULT 0 CHECK(agent_snd_file_deleted NOT NULL), - protocol TEXT NOT NULL DEFAULT 'smp' + protocol TEXT NOT NULL DEFAULT 'smp', + file_crypto_key BLOB, + file_crypto_nonce BLOB ); CREATE TABLE snd_files( file_id INTEGER NOT NULL REFERENCES files ON DELETE CASCADE, @@ -283,6 +260,9 @@ CREATE TABLE connections( security_code TEXT NULL, security_code_verified_at TEXT NULL, auth_err_counter INTEGER DEFAULT 0 CHECK(auth_err_counter NOT NULL), + peer_chat_min_version INTEGER NOT NULL DEFAULT 1, + peer_chat_max_version INTEGER NOT NULL DEFAULT 1, + to_subscribe INTEGER DEFAULT 0 NOT NULL, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE @@ -316,6 +296,8 @@ CREATE TABLE contact_requests( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, updated_at TEXT CHECK(updated_at NOT NULL), xcontact_id BLOB, + peer_chat_min_version INTEGER NOT NULL DEFAULT 1, + peer_chat_max_version INTEGER NOT NULL DEFAULT 1, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON UPDATE CASCADE @@ -496,6 +478,43 @@ CREATE TABLE chat_item_moderations( created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) ); +CREATE TABLE group_snd_item_statuses( + group_snd_item_status_id INTEGER PRIMARY KEY, + chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, + group_member_id INTEGER NOT NULL REFERENCES group_members ON DELETE CASCADE, + group_snd_item_status TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT(datetime('now')), + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +CREATE TABLE IF NOT EXISTS "sent_probes"( + sent_probe_id INTEGER PRIMARY KEY, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + probe BLOB NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL), + UNIQUE(user_id, probe) +); +CREATE TABLE IF NOT EXISTS "sent_probe_hashes"( + sent_probe_hash_id INTEGER PRIMARY KEY, + sent_probe_id INTEGER NOT NULL REFERENCES "sent_probes" ON DELETE CASCADE, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); +CREATE TABLE IF NOT EXISTS "received_probes"( + received_probe_id INTEGER PRIMARY KEY, + contact_id INTEGER REFERENCES contacts ON DELETE CASCADE, + group_member_id INTEGER REFERENCES group_members ON DELETE CASCADE, + probe BLOB, + probe_hash BLOB NOT NULL, + user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, + created_at TEXT CHECK(created_at NOT NULL), + updated_at TEXT CHECK(updated_at NOT NULL) +); CREATE INDEX contact_profiles_index ON contact_profiles( display_name, full_name @@ -609,10 +628,6 @@ CREATE INDEX idx_pending_group_messages_group_member_id ON pending_group_message ); CREATE INDEX idx_rcv_file_chunks_file_id ON rcv_file_chunks(file_id); CREATE INDEX idx_rcv_files_group_member_id ON rcv_files(group_member_id); -CREATE INDEX idx_received_probes_user_id ON received_probes(user_id); -CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id); -CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id); -CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id); CREATE INDEX idx_settings_user_id ON settings(user_id); CREATE INDEX idx_snd_file_chunks_file_id_connection_id ON snd_file_chunks( file_id, @@ -687,3 +702,32 @@ CREATE INDEX idx_chat_item_moderations_group ON chat_item_moderations( item_member_id, shared_msg_id ); +CREATE INDEX idx_group_snd_item_statuses_chat_item_id ON group_snd_item_statuses( + chat_item_id +); +CREATE INDEX idx_group_snd_item_statuses_group_member_id ON group_snd_item_statuses( + group_member_id +); +CREATE INDEX idx_chat_items_user_id_item_status ON chat_items( + user_id, + item_status +); +CREATE INDEX idx_connections_to_subscribe ON connections(to_subscribe); +CREATE INDEX idx_contacts_contact_group_member_id ON contacts( + contact_group_member_id +); +CREATE INDEX idx_sent_probes_user_id ON sent_probes(user_id); +CREATE INDEX idx_sent_probes_contact_id ON sent_probes(contact_id); +CREATE INDEX idx_sent_probes_group_member_id ON sent_probes(group_member_id); +CREATE INDEX idx_sent_probe_hashes_user_id ON sent_probe_hashes(user_id); +CREATE INDEX idx_sent_probe_hashes_sent_probe_id ON sent_probe_hashes( + sent_probe_id +); +CREATE INDEX idx_sent_probe_hashes_contact_id ON sent_probe_hashes(contact_id); +CREATE INDEX idx_sent_probe_hashes_group_member_id ON sent_probe_hashes( + group_member_id +); +CREATE INDEX idx_received_probes_user_id ON received_probes(user_id); +CREATE INDEX idx_received_probes_contact_id ON received_probes(contact_id); +CREATE INDEX idx_received_probes_probe ON received_probes(probe); +CREATE INDEX idx_received_probes_probe_hash ON received_probes(probe_hash); diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 85900a6d72..700548bb12 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -14,14 +14,12 @@ import Data.Aeson (ToJSON (..)) import qualified Data.Aeson as J import Data.Bifunctor (first) import qualified Data.ByteString.Base64.URL as U +import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B -import qualified Data.ByteString.Lazy.Char8 as LB import Data.Functor (($>)) import Data.List (find) import qualified Data.List.NonEmpty as L import Data.Maybe (fromMaybe) -import qualified Data.Text as T -import Data.Text.Encoding (encodeUtf8) import Data.Word (Word8) import Database.SQLite.Simple (SQLError (..)) import qualified Database.SQLite.Simple as DB @@ -30,10 +28,13 @@ import Foreign.C.Types (CInt (..)) import Foreign.Ptr import Foreign.StablePtr import Foreign.Storable (poke) +import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding) import GHC.Generics (Generic) import Simplex.Chat import Simplex.Chat.Controller import Simplex.Chat.Markdown (ParsedMarkdown (..), parseMaybeMarkdownList) +import Simplex.Chat.Mobile.File +import Simplex.Chat.Mobile.Shared import Simplex.Chat.Mobile.WebRTC import Simplex.Chat.Options import Simplex.Chat.Store @@ -47,6 +48,7 @@ import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..)) import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8) +import System.IO (utf8) import System.Timeout (timeout) foreign export ccall "chat_migrate_init" cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString @@ -67,9 +69,23 @@ foreign export ccall "chat_encrypt_media" cChatEncryptMedia :: CString -> Ptr Wo foreign export ccall "chat_decrypt_media" cChatDecryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString +foreign export ccall "chat_write_file" cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString + +foreign export ccall "chat_read_file" cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) + +foreign export ccall "chat_encrypt_file" cChatEncryptFile :: CString -> CString -> IO CJSONString + +foreign export ccall "chat_decrypt_file" cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString + -- | check / migrate database and initialize chat controller on success cChatMigrateInit :: CString -> CString -> CString -> Ptr (StablePtr ChatController) -> IO CJSONString cChatMigrateInit fp key conf ctrl = do + -- ensure we are set to UTF-8; iOS does not have locale, and will default to + -- US-ASCII all the time. + setLocaleEncoding utf8 + setFileSystemEncoding utf8 + setForeignEncoding utf8 + dbPath <- peekCAString fp dbKey <- peekCAString key confirm <- peekCAString conf @@ -77,36 +93,36 @@ cChatMigrateInit fp key conf ctrl = do chatMigrateInit dbPath dbKey confirm >>= \case Right cc -> (newStablePtr cc >>= poke ctrl) $> DBMOk Left e -> pure e - newCAString . LB.unpack $ J.encode r + newCStringFromLazyBS $ J.encode r -- | send command to chat (same syntax as in terminal for now) cChatSendCmd :: StablePtr ChatController -> CString -> IO CJSONString cChatSendCmd cPtr cCmd = do c <- deRefStablePtr cPtr - cmd <- peekCAString cCmd - newCAString =<< chatSendCmd c cmd + cmd <- B.packCString cCmd + newCStringFromLazyBS =<< chatSendCmd c cmd -- | receive message from chat (blocking) cChatRecvMsg :: StablePtr ChatController -> IO CJSONString -cChatRecvMsg cc = deRefStablePtr cc >>= chatRecvMsg >>= newCAString +cChatRecvMsg cc = deRefStablePtr cc >>= chatRecvMsg >>= newCStringFromLazyBS -- | receive message from chat (blocking up to `t` microseconds (1/10^6 sec), returns empty string if times out) cChatRecvMsgWait :: StablePtr ChatController -> CInt -> IO CJSONString -cChatRecvMsgWait cc t = deRefStablePtr cc >>= (`chatRecvMsgWait` fromIntegral t) >>= newCAString +cChatRecvMsgWait cc t = deRefStablePtr cc >>= (`chatRecvMsgWait` fromIntegral t) >>= newCStringFromLazyBS -- | parse markdown - returns ParsedMarkdown type JSON cChatParseMarkdown :: CString -> IO CJSONString -cChatParseMarkdown s = newCAString . chatParseMarkdown =<< peekCAString s +cChatParseMarkdown s = newCStringFromLazyBS . chatParseMarkdown =<< B.packCString s -- | parse server address - returns ParsedServerAddress JSON cChatParseServer :: CString -> IO CJSONString -cChatParseServer s = newCAString . chatParseServer =<< peekCAString s +cChatParseServer s = newCStringFromLazyBS . chatParseServer =<< B.packCString s cChatPasswordHash :: CString -> CString -> IO CString cChatPasswordHash cPwd cSalt = do - pwd <- peekCAString cPwd - salt <- peekCAString cSalt - newCAString $ chatPasswordHash pwd salt + pwd <- B.packCString cPwd + salt <- B.packCString cSalt + newCStringFromBS $ chatPasswordHash pwd salt mobileChatOpts :: String -> String -> ChatOpts mobileChatOpts dbFilePrefix dbKey = @@ -143,8 +159,6 @@ defaultMobileConfig = logLevel = CLLError } -type CJSONString = CString - getActiveUser_ :: SQLiteStore -> IO (Maybe User) getActiveUser_ st = find activeUser <$> withTransaction st getUsers @@ -181,22 +195,22 @@ chatMigrateInit dbFilePrefix dbKey confirm = runExceptT $ do _ -> dbError e dbError e = Left . DBMErrorSQL dbFile $ show e -chatSendCmd :: ChatController -> String -> IO JSONString -chatSendCmd cc s = LB.unpack . J.encode . APIResponse Nothing <$> runReaderT (execChatCommand $ B.pack s) cc +chatSendCmd :: ChatController -> ByteString -> IO JSONByteString +chatSendCmd cc s = J.encode . APIResponse Nothing <$> runReaderT (execChatCommand s) cc -chatRecvMsg :: ChatController -> IO JSONString +chatRecvMsg :: ChatController -> IO JSONByteString chatRecvMsg ChatController {outputQ} = json <$> atomically (readTBQueue outputQ) where - json (corr, resp) = LB.unpack $ J.encode APIResponse {corr, resp} + json (corr, resp) = J.encode APIResponse {corr, resp} -chatRecvMsgWait :: ChatController -> Int -> IO JSONString +chatRecvMsgWait :: ChatController -> Int -> IO JSONByteString chatRecvMsgWait cc time = fromMaybe "" <$> timeout time (chatRecvMsg cc) -chatParseMarkdown :: String -> JSONString -chatParseMarkdown = LB.unpack . J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 . B.pack +chatParseMarkdown :: ByteString -> JSONByteString +chatParseMarkdown = J.encode . ParsedMarkdown . parseMaybeMarkdownList . safeDecodeUtf8 -chatParseServer :: String -> JSONString -chatParseServer = LB.unpack . J.encode . toServerAddress . strDecode . B.pack +chatParseServer :: ByteString -> JSONByteString +chatParseServer = J.encode . toServerAddress . strDecode where toServerAddress :: Either String AProtoServerWithAuth -> ParsedServerAddress toServerAddress = \case @@ -207,11 +221,11 @@ chatParseServer = LB.unpack . J.encode . toServerAddress . strDecode . B.pack enc :: StrEncoding a => a -> String enc = B.unpack . strEncode -chatPasswordHash :: String -> String -> String +chatPasswordHash :: ByteString -> ByteString -> ByteString chatPasswordHash pwd salt = either (const "") passwordHash salt' where - salt' = U.decode $ B.pack salt - passwordHash = B.unpack . U.encode . C.sha512Hash . (encodeUtf8 (T.pack pwd) <>) + salt' = U.decode salt + passwordHash = U.encode . C.sha512Hash . (pwd <>) data APIResponse = APIResponse {corr :: Maybe CorrId, resp :: ChatResponse} deriving (Generic) diff --git a/src/Simplex/Chat/Mobile/File.hs b/src/Simplex/Chat/Mobile/File.hs new file mode 100644 index 0000000000..f385e14f4d --- /dev/null +++ b/src/Simplex/Chat/Mobile/File.hs @@ -0,0 +1,141 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} + +module Simplex.Chat.Mobile.File + ( cChatWriteFile, + cChatReadFile, + cChatEncryptFile, + cChatDecryptFile, + WriteFileResult (..), + ReadFileResult (..), + chatWriteFile, + chatReadFile, + ) +where + +import Control.Monad.Except +import Data.Aeson (ToJSON) +import qualified Data.Aeson as J +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +import qualified Data.ByteString.Lazy as LB +import qualified Data.ByteString.Lazy.Char8 as LB' +import Data.Char (chr) +import Data.Either (fromLeft) +import Data.Word (Word32, Word8) +import Foreign.C +import Foreign.Marshal.Alloc (mallocBytes) +import Foreign.Ptr +import Foreign.Storable (poke, pokeByteOff) +import GHC.Generics (Generic) +import Simplex.Chat.Mobile.Shared +import Simplex.Chat.Util (chunkSize, encryptFile) +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..), CryptoFileHandle, FTCryptoError (..)) +import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import Simplex.Messaging.Util (catchAll) +import UnliftIO (Handle, IOMode (..), withFile) + +data WriteFileResult + = WFResult {cryptoArgs :: CryptoFileArgs} + | WFError {writeError :: String} + deriving (Generic) + +instance ToJSON WriteFileResult where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "WF" + +cChatWriteFile :: CString -> Ptr Word8 -> CInt -> IO CJSONString +cChatWriteFile cPath ptr len = do + path <- peekCString cPath + s <- getByteString ptr len + r <- chatWriteFile path s + newCStringFromLazyBS $ J.encode r + +chatWriteFile :: FilePath -> ByteString -> IO WriteFileResult +chatWriteFile path s = do + cfArgs <- CF.randomArgs + let file = CryptoFile path $ Just cfArgs + either WFError (\_ -> WFResult cfArgs) + <$> runCatchExceptT (withExceptT show $ CF.writeFile file $ LB.fromStrict s) + +data ReadFileResult + = RFResult {fileSize :: Int} + | RFError {readError :: String} + deriving (Generic) + +instance ToJSON ReadFileResult where toEncoding = J.genericToEncoding . sumTypeJSON $ dropPrefix "RF" + +cChatReadFile :: CString -> CString -> CString -> IO (Ptr Word8) +cChatReadFile cPath cKey cNonce = do + path <- peekCString cPath + key <- B.packCString cKey + nonce <- B.packCString cNonce + chatReadFile path key nonce >>= \case + Left e -> castPtr <$> newCString (chr 1 : e) + Right s -> do + let len = fromIntegral $ LB.length s + ptr <- mallocBytes $ len + 5 + poke ptr (0 :: Word8) + pokeByteOff ptr 1 (fromIntegral len :: Word32) + putLazyByteString (ptr `plusPtr` 5) s + pure ptr + +chatReadFile :: FilePath -> ByteString -> ByteString -> IO (Either String LB.ByteString) +chatReadFile path keyStr nonceStr = runCatchExceptT $ do + key <- liftEither $ strDecode keyStr + nonce <- liftEither $ strDecode nonceStr + let file = CryptoFile path $ Just $ CFArgs key nonce + withExceptT show $ CF.readFile file + +cChatEncryptFile :: CString -> CString -> IO CJSONString +cChatEncryptFile cFromPath cToPath = do + fromPath <- peekCString cFromPath + toPath <- peekCString cToPath + r <- chatEncryptFile fromPath toPath + newCAString . LB'.unpack $ J.encode r + +chatEncryptFile :: FilePath -> FilePath -> IO WriteFileResult +chatEncryptFile fromPath toPath = + either WFError WFResult <$> runCatchExceptT encrypt + where + encrypt = do + cfArgs <- liftIO $ CF.randomArgs + encryptFile fromPath toPath cfArgs + pure cfArgs + +cChatDecryptFile :: CString -> CString -> CString -> CString -> IO CString +cChatDecryptFile cFromPath cKey cNonce cToPath = do + fromPath <- peekCString cFromPath + key <- B.packCString cKey + nonce <- B.packCString cNonce + toPath <- peekCString cToPath + r <- chatDecryptFile fromPath key nonce toPath + newCAString r + +chatDecryptFile :: FilePath -> ByteString -> ByteString -> FilePath -> IO String +chatDecryptFile fromPath keyStr nonceStr toPath = fromLeft "" <$> runCatchExceptT decrypt + where + decrypt = do + key <- liftEither $ strDecode keyStr + nonce <- liftEither $ strDecode nonceStr + let fromFile = CryptoFile fromPath $ Just $ CFArgs key nonce + size <- liftIO $ CF.getFileContentsSize fromFile + withExceptT show $ + CF.withFile fromFile ReadMode $ \r -> withFile toPath WriteMode $ \w -> do + decryptChunks r w size + CF.hGetTag r + decryptChunks :: CryptoFileHandle -> Handle -> Integer -> ExceptT FTCryptoError IO () + decryptChunks r w !size = do + let chSize = min size chunkSize + chSize' = fromIntegral chSize + size' = size - chSize + ch <- liftIO $ CF.hGet r chSize' + when (B.length ch /= chSize') $ throwError $ FTCEFileIOError "encrypting file: unexpected EOF" + liftIO $ B.hPut w ch + when (size' > 0) $ decryptChunks r w size' + +runCatchExceptT :: ExceptT String IO a -> IO (Either String a) +runCatchExceptT action = runExceptT action `catchAll` (pure . Left . show) diff --git a/src/Simplex/Chat/Mobile/Shared.hs b/src/Simplex/Chat/Mobile/Shared.hs new file mode 100644 index 0000000000..d1f60ffce5 --- /dev/null +++ b/src/Simplex/Chat/Mobile/Shared.hs @@ -0,0 +1,48 @@ +{-# LANGUAGE LambdaCase #-} + +module Simplex.Chat.Mobile.Shared where + +import qualified Data.ByteString as B +import Data.ByteString.Internal (ByteString (..), memcpy) +import qualified Data.ByteString.Lazy as LB +import qualified Data.ByteString.Lazy.Internal as LB +import Foreign.C (CInt, CString) +import Foreign + +type CJSONString = CString + +type JSONByteString = LB.ByteString + +getByteString :: Ptr Word8 -> CInt -> IO ByteString +getByteString ptr len = do + fp <- newForeignPtr_ ptr + pure $ PS fp 0 $ fromIntegral len +{-# INLINE getByteString #-} + +putByteString :: Ptr Word8 -> ByteString -> IO () +putByteString ptr (PS fp offset len) = + withForeignPtr fp $ \p -> memcpy ptr (p `plusPtr` offset) len +{-# INLINE putByteString #-} + +putLazyByteString :: Ptr Word8 -> LB.ByteString -> IO () +putLazyByteString ptr = \case + LB.Empty -> pure () + LB.Chunk ch s -> do + putByteString ptr ch + putLazyByteString (ptr `plusPtr` B.length ch) s + +newCStringFromBS :: ByteString -> IO CString +newCStringFromBS s = do + let len = B.length s + buf <- mallocBytes (len + 1) + putByteString buf s + pokeByteOff buf len (0 :: Word8) + pure $ castPtr buf + +newCStringFromLazyBS :: LB.ByteString -> IO CString +newCStringFromLazyBS s = do + let len = fromIntegral $ LB.length s + buf <- mallocBytes (len + 1) + putLazyByteString buf s + pokeByteOff buf len (0 :: Word8) + pure $ castPtr buf diff --git a/src/Simplex/Chat/Mobile/WebRTC.hs b/src/Simplex/Chat/Mobile/WebRTC.hs index e05c9d609e..19ba2b751b 100644 --- a/src/Simplex/Chat/Mobile/WebRTC.hs +++ b/src/Simplex/Chat/Mobile/WebRTC.hs @@ -12,16 +12,15 @@ import Control.Monad.Except import qualified Crypto.Cipher.Types as AES import Data.Bifunctor (bimap) import qualified Data.ByteArray as BA +import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Base64.URL as U -import Data.ByteString.Internal (ByteString (PS), memcpy) import Data.Either (fromLeft) import Data.Word (Word8) import Foreign.C (CInt, CString, newCAString) -import Foreign.ForeignPtr (newForeignPtr_) -import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr) -import Foreign.Ptr (Ptr, plusPtr) +import Foreign.Ptr (Ptr) import qualified Simplex.Messaging.Crypto as C +import Simplex.Chat.Mobile.Shared cChatEncryptMedia :: CString -> Ptr Word8 -> CInt -> IO CString cChatEncryptMedia = cTransformMedia chatEncryptMedia @@ -32,16 +31,10 @@ cChatDecryptMedia = cTransformMedia chatDecryptMedia cTransformMedia :: (ByteString -> ByteString -> ExceptT String IO ByteString) -> CString -> Ptr Word8 -> CInt -> IO CString cTransformMedia f cKey cFrame cFrameLen = do key <- B.packCString cKey - frame <- getFrame + frame <- getByteString cFrame cFrameLen runExceptT (f key frame >>= liftIO . putFrame) >>= newCAString . fromLeft "" where - getFrame = do - fp <- newForeignPtr_ cFrame - pure $ PS fp 0 $ fromIntegral cFrameLen - putFrame bs@(PS fp offset _) = do - let len = B.length bs - p = unsafeForeignPtrToPtr fp `plusPtr` offset - when (len <= fromIntegral cFrameLen) $ memcpy cFrame p len + putFrame s = when (B.length s <= fromIntegral cFrameLen) $ putByteString cFrame s {-# INLINE cTransformMedia #-} chatEncryptMedia :: ByteString -> ByteString -> ExceptT String IO ByteString diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 31d1eb5738..660f52cdff 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -46,6 +46,21 @@ import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Version hiding (version) + +currentChatVersion :: Version +currentChatVersion = 2 + +supportedChatVRange :: VersionRange +supportedChatVRange = mkVersionRange 1 currentChatVersion + +-- version range that supports skipping establishing direct connections in a group +groupNoDirectVRange :: VersionRange +groupNoDirectVRange = mkVersionRange 2 currentChatVersion + +-- version range that supports establishing direct connection via x.grp.direct.inv with a group member +xGrpDirectInvVRange :: VersionRange +xGrpDirectInvVRange = mkVersionRange 2 currentChatVersion data ConnectionEntity = RcvDirectMsgConnection {entityConnection :: Connection, contact :: Maybe Contact} @@ -102,7 +117,8 @@ data AppMessage (e :: MsgEncoding) where -- chat message is sent as JSON with these properties data AppMessageJson = AppMessageJson - { msgId :: Maybe SharedMsgId, + { v :: Maybe ChatVersionRange, + msgId :: Maybe SharedMsgId, event :: Text, params :: J.Object } @@ -161,7 +177,11 @@ instance ToJSON MsgRef where toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} -data ChatMessage e = ChatMessage {msgId :: Maybe SharedMsgId, chatMsgEvent :: ChatMsgEvent e} +data ChatMessage e = ChatMessage + { chatVRange :: VersionRange, + msgId :: Maybe SharedMsgId, + chatMsgEvent :: ChatMsgEvent e + } deriving (Eq, Show) data AChatMessage = forall e. MsgEncodingI e => ACMsg (SMsgEncoding e) (ChatMessage e) @@ -207,6 +227,7 @@ data ChatMsgEvent (e :: MsgEncoding) where XGrpLeave :: ChatMsgEvent 'Json XGrpDel :: ChatMsgEvent 'Json XGrpInfo :: GroupProfile -> ChatMsgEvent 'Json + XGrpDirectInv :: ConnReqInvitation -> Maybe MsgContent -> ChatMsgEvent 'Json XInfoProbe :: Probe -> ChatMsgEvent 'Json XInfoProbeCheck :: ProbeHash -> ChatMsgEvent 'Json XInfoProbeOk :: Probe -> ChatMsgEvent 'Json @@ -541,6 +562,7 @@ data CMEventTag (e :: MsgEncoding) where XGrpLeave_ :: CMEventTag 'Json XGrpDel_ :: CMEventTag 'Json XGrpInfo_ :: CMEventTag 'Json + XGrpDirectInv_ :: CMEventTag 'Json XInfoProbe_ :: CMEventTag 'Json XInfoProbeCheck_ :: CMEventTag 'Json XInfoProbeOk_ :: CMEventTag 'Json @@ -586,6 +608,7 @@ instance MsgEncodingI e => StrEncoding (CMEventTag e) where XGrpLeave_ -> "x.grp.leave" XGrpDel_ -> "x.grp.del" XGrpInfo_ -> "x.grp.info" + XGrpDirectInv_ -> "x.grp.direct.inv" XInfoProbe_ -> "x.info.probe" XInfoProbeCheck_ -> "x.info.probe.check" XInfoProbeOk_ -> "x.info.probe.ok" @@ -632,6 +655,7 @@ instance StrEncoding ACMEventTag where "x.grp.leave" -> XGrpLeave_ "x.grp.del" -> XGrpDel_ "x.grp.info" -> XGrpInfo_ + "x.grp.direct.inv" -> XGrpDirectInv_ "x.info.probe" -> XInfoProbe_ "x.info.probe.check" -> XInfoProbeCheck_ "x.info.probe.ok" -> XInfoProbeOk_ @@ -674,6 +698,7 @@ toCMEventTag msg = case msg of XGrpLeave -> XGrpLeave_ XGrpDel -> XGrpDel_ XGrpInfo _ -> XGrpInfo_ + XGrpDirectInv _ _ -> XGrpDirectInv_ XInfoProbe _ -> XInfoProbe_ XInfoProbeCheck _ -> XInfoProbeCheck_ XInfoProbeOk _ -> XInfoProbeOk_ @@ -724,17 +749,17 @@ appBinaryToCM :: AppMessageBinary -> Either String (ChatMessage 'Binary) appBinaryToCM AppMessageBinary {msgId, tag, body} = do eventTag <- strDecode $ B.singleton tag chatMsgEvent <- parseAll (msg eventTag) body - pure ChatMessage {msgId, chatMsgEvent} + pure ChatMessage {chatVRange = chatInitialVRange, msgId, chatMsgEvent} where msg :: CMEventTag 'Binary -> A.Parser (ChatMsgEvent 'Binary) msg = \case BFileChunk_ -> BFileChunk <$> (SharedMsgId <$> smpP) <*> (unIFC <$> smpP) appJsonToCM :: AppMessageJson -> Either String (ChatMessage 'Json) -appJsonToCM AppMessageJson {msgId, event, params} = do +appJsonToCM AppMessageJson {v, msgId, event, params} = do eventTag <- strDecode $ encodeUtf8 event chatMsgEvent <- msg eventTag - pure ChatMessage {msgId, chatMsgEvent} + pure ChatMessage {chatVRange = maybe chatInitialVRange fromChatVRange v, msgId, chatMsgEvent} where p :: FromJSON a => J.Key -> Either String a p key = JT.parseEither (.: key) params @@ -769,6 +794,7 @@ appJsonToCM AppMessageJson {msgId, event, params} = do XGrpLeave_ -> pure XGrpLeave XGrpDel_ -> pure XGrpDel XGrpInfo_ -> XGrpInfo <$> p "groupProfile" + XGrpDirectInv_ -> XGrpDirectInv <$> p "connReq" <*> opt "content" XInfoProbe_ -> XInfoProbe <$> p "probe" XInfoProbeCheck_ -> XInfoProbeCheck <$> p "probeHash" XInfoProbeOk_ -> XInfoProbeOk <$> p "probe" @@ -784,11 +810,11 @@ appJsonToCM AppMessageJson {msgId, event, params} = do key .=? value = maybe id ((:) . (key .=)) value chatToAppMessage :: forall e. MsgEncodingI e => ChatMessage e -> AppMessage e -chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of +chatToAppMessage ChatMessage {chatVRange, msgId, chatMsgEvent} = case encoding @e of SBinary -> let (binaryMsgId, body) = toBody chatMsgEvent in AMBinary AppMessageBinary {msgId = binaryMsgId, tag = B.head $ strEncode tag, body} - SJson -> AMJson AppMessageJson {msgId, event = textEncode tag, params = params chatMsgEvent} + SJson -> AMJson AppMessageJson {v = Just $ ChatVersionRange chatVRange, msgId, event = textEncode tag, params = params chatMsgEvent} where tag = toCMEventTag chatMsgEvent o :: [(J.Key, J.Value)] -> J.Object @@ -804,7 +830,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of XMsgUpdate msgId' content ttl live -> o $ ("ttl" .=? ttl) $ ("live" .=? live) ["msgId" .= msgId', "content" .= content] XMsgDel msgId' memberId -> o $ ("memberId" .=? memberId) ["msgId" .= msgId'] XMsgDeleted -> JM.empty - XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add] + XMsgReact msgId' memberId reaction add -> o $ ("memberId" .=? memberId) ["msgId" .= msgId', "reaction" .= reaction, "add" .= add] XFile fileInv -> o ["file" .= fileInv] XFileAcpt fileName -> o ["fileName" .= fileName] XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName] @@ -825,6 +851,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of XGrpLeave -> JM.empty XGrpDel -> JM.empty XGrpInfo p -> o ["groupProfile" .= p] + XGrpDirectInv connReq content -> o $ ("content" .=? content) ["connReq" .= connReq] XInfoProbe probe -> o ["probe" .= probe] XInfoProbeCheck probeHash -> o ["probeHash" .= probeHash] XInfoProbeOk probe -> o ["probe" .= probe] diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 4b66291fe9..842c57838e 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} @@ -6,25 +7,30 @@ module Simplex.Chat.Store.Connections ( getConnectionEntity, + getConnectionsToSubscribe, + unsetConnectionToSubscribe, ) where import Control.Applicative ((<|>)) import Control.Monad.Except import Data.Int (Int64) -import Data.Maybe (fromMaybe) +import Data.Maybe (catMaybes, fromMaybe) import Data.Text (Text) import Data.Time.Clock (UTCTime (..)) -import Database.SQLite.Simple ((:.) (..)) -import qualified Database.SQLite.Simple as DB +import Database.SQLite.Simple (Only (..), (:.) (..)) import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Store.Files import Simplex.Chat.Store.Groups +import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow') +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Util (eitherToMaybe) getConnectionEntity :: DB.Connection -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity getConnectionEntity db user@User {userId, userContactId} agentConnId = do @@ -49,7 +55,8 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter + conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter, + peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND agent_conn_id = ? |] @@ -62,18 +69,18 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do [sql| SELECT c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 |] (userId, contactId) - toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)] -> Either StoreError Contact - toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs)] = + toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact + toContact' contactId activeConn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn - in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs} + in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = ExceptT $ do @@ -141,3 +148,17 @@ getConnectionEntity db user@User {userId, userContactId} agentConnId = do userContact_ :: [(ConnReqContact, Maybe GroupId)] -> Either StoreError UserContact userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId} userContact_ _ = Left SEUserContactLinkNotFound + +getConnectionsToSubscribe :: DB.Connection -> IO ([ConnId], [ConnectionEntity]) +getConnectionsToSubscribe db = do + aConnIds <- map fromOnly <$> DB.query_ db "SELECT agent_conn_id FROM connections where to_subscribe = 1" + entities <- forM aConnIds $ \acId -> do + getUserByAConnId db acId >>= \case + Just user -> eitherToMaybe <$> runExceptT (getConnectionEntity db user acId) + Nothing -> pure Nothing + unsetConnectionToSubscribe db + let connIds = map (\(AgentConnId connId) -> connId) aConnIds + pure (connIds, catMaybes entities) + +unsetConnectionToSubscribe :: DB.Connection -> IO () +unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1" diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 944527ae48..9c45129385 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -12,11 +12,13 @@ module Simplex.Chat.Store.Direct updateContactProfile_, updateContactProfile_', deleteContactProfile_, + deleteUnusedProfile_, -- * Contacts and connections functions getPendingContactConnection, deletePendingContactConnection, createDirectConnection, + createIncognitoProfile, createConnReqConnection, getProfileById, getConnReqContactXContactId, @@ -33,6 +35,8 @@ module Simplex.Chat.Store.Direct updateContactUserPreferences, updateContactAlias, updateContactConnectionAlias, + updatePCCIncognito, + deletePCCIncognitoProfile, updateContactUsed, updateContactUnreadChat, updateGroupUnreadChat, @@ -65,13 +69,15 @@ import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) -import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Version getPendingContactConnection :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO PendingContactConnection getPendingContactConnection db userId connId = do @@ -105,8 +111,8 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let pccConnStatus = ConnJoined @@ -115,10 +121,10 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, - via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?) + via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) |] - ((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt)) + ((userId, acId, pccConnStatus, ConnContact, cReqHash, xContactId) :. (customUserProfileId, isJust groupLinkId, groupLinkId, createdAt, createdAt, subMode == SMOnlyCreate)) pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} @@ -137,10 +143,11 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id JOIN connections c ON c.contact_id = ct.contact_id @@ -157,20 +164,25 @@ getConnReqContactXContactId db user@User {userId} cReqHash = do "SELECT xcontact_id FROM connections WHERE user_id = ? AND via_contact_uri_hash = ? LIMIT 1" (userId, cReqHash) -createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> IO PendingContactConnection -createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile = do +createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> IO PendingContactConnection +createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile DB.execute db [sql| INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, custom_user_profile_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?) + (user_id, agent_conn_id, conn_req_inv, conn_status, conn_type, custom_user_profile_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?,?) |] - (userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt) + (userId, acId, cReq, pccConnStatus, ConnContact, customUserProfileId, createdAt, createdAt, subMode == SMOnlyCreate) pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} +createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 +createIncognitoProfile db User {userId} p = do + createdAt <- getCurrentTime + createIncognitoProfile_ db userId createdAt p + createIncognitoProfile_ :: DB.Connection -> UserId -> UTCTime -> Profile -> IO Int64 createIncognitoProfile_ db userId createdAt Profile {displayName, fullName, image} = do DB.execute @@ -189,7 +201,7 @@ createDirectContact db user@User {userId} activeConn@Connection {connId, localAl let profile = toLocalProfile profileId p localAlias userPreferences = emptyChatPrefs mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn - pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt, chatTs = Just createdAt} + pure $ Contact {contactId, localDisplayName, profile, activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False} deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnectionsAndFiles db userId Contact {contactId} = do @@ -256,6 +268,34 @@ deleteContactProfile_ db userId contactId = |] (userId, contactId) +deleteUnusedProfile_ :: DB.Connection -> UserId -> ProfileId -> IO () +deleteUnusedProfile_ db userId profileId = + DB.executeNamed + db + [sql| + DELETE FROM contact_profiles + WHERE user_id = :user_id AND contact_profile_id = :profile_id + AND 1 NOT IN ( + SELECT 1 FROM connections + WHERE user_id = :user_id AND custom_user_profile_id = :profile_id LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contacts + WHERE user_id = :user_id AND contact_profile_id = :profile_id LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contact_requests + WHERE user_id = :user_id AND contact_profile_id = :profile_id LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM group_members + WHERE user_id = :user_id + AND (member_profile_id = :profile_id OR contact_profile_id = :profile_id) + LIMIT 1 + ) + |] + [":user_id" := userId, ":profile_id" := profileId] + updateContactProfile :: DB.Connection -> User -> Contact -> Profile -> ExceptT StoreError IO Contact updateContactProfile db user@User {userId} c p' | displayName == newName = do @@ -307,7 +347,30 @@ updateContactConnectionAlias db userId conn localAlias = do WHERE user_id = ? AND connection_id = ? |] (localAlias, updatedAt, userId, pccConnId conn) - pure (conn :: PendingContactConnection) {localAlias} + pure (conn :: PendingContactConnection) {localAlias, updatedAt} + +updatePCCIncognito :: DB.Connection -> User -> PendingContactConnection -> Maybe ProfileId -> IO PendingContactConnection +updatePCCIncognito db User {userId} conn customUserProfileId = do + updatedAt <- getCurrentTime + DB.execute + db + [sql| + UPDATE connections + SET custom_user_profile_id = ?, updated_at = ? + WHERE user_id = ? AND connection_id = ? + |] + (customUserProfileId, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} + +deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () +deletePCCIncognitoProfile db User {userId} profileId = + DB.execute + db + [sql| + DELETE FROM contact_profiles + WHERE user_id = ? AND contact_profile_id = ? AND incognito = 1 + |] + (userId, profileId) updateContactUsed :: DB.Connection -> User -> Contact -> IO () updateContactUsed db User {userId} Contact {contactId} = do @@ -380,8 +443,8 @@ getUserContacts db user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) rights <$> mapM (runExceptT . getContact db user) contactIds -createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest -createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profile {displayName, fullName, image, contactLink, preferences} xContactId_ = +createOrUpdateContactRequest :: DB.Connection -> User -> Int64 -> InvitationId -> VersionRange -> Profile -> Maybe XContactId -> ExceptT StoreError IO ContactOrRequest +createOrUpdateContactRequest db user@User {userId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ = liftIO (maybeM getContact' xContactId_) >>= \case Just contact -> pure $ CORContact contact Nothing -> CORRequest <$> createOrUpdate_ @@ -410,10 +473,10 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi db [sql| INSERT INTO contact_requests - (user_contact_link_id, agent_invitation_id, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id) - VALUES (?,?,?,?,?,?,?,?) + (user_contact_link_id, agent_invitation_id, peer_chat_min_version, peer_chat_max_version, contact_profile_id, local_display_name, user_id, created_at, updated_at, xcontact_id) + VALUES (?,?,?,?,?,?,?,?,?,?) |] - (userContactLinkId, invId, profileId, ldn, userId, currentTs, currentTs, xContactId_) + (userContactLinkId, invId, minV, maxV, profileId, ldn, userId, currentTs, currentTs, xContactId_) insertedRowId db getContact' :: XContactId -> IO (Maybe Contact) getContact' xContactId = @@ -424,10 +487,11 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id @@ -444,7 +508,8 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) @@ -458,10 +523,26 @@ createOrUpdateContactRequest db user@User {userId} userContactLinkId invId Profi currentTs <- liftIO getCurrentTime updateProfile currentTs if displayName == oldDisplayName - then Right <$> DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, currentTs, userId, cReqId) + then + Right + <$> DB.execute + db + [sql| + UPDATE contact_requests + SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + |] + (invId, minV, maxV, currentTs, userId, cReqId) else withLocalDisplayName db userId displayName $ \ldn -> Right <$> do - DB.execute db "UPDATE contact_requests SET agent_invitation_id = ?, local_display_name = ?, updated_at = ? WHERE user_id = ? AND contact_request_id = ?" (invId, ldn, currentTs, userId, cReqId) + DB.execute + db + [sql| + UPDATE contact_requests + SET agent_invitation_id = ?, peer_chat_min_version = ?, peer_chat_max_version = ?, local_display_name = ?, updated_at = ? + WHERE user_id = ? AND contact_request_id = ? + |] + (invId, minV, maxV, ldn, currentTs, userId, cReqId) DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (oldLdn, userId) where updateProfile currentTs = @@ -496,7 +577,8 @@ getContactRequest db User {userId} contactRequestId = [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c USING (user_contact_link_id) JOIN contact_profiles p USING (contact_profile_id) @@ -535,8 +617,8 @@ deleteContactRequest db User {userId} contactRequestId = do (userId, userId, contactRequestId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> User -> ConnId -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> IO Contact -createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId localDisplayName profileId profile userContactLinkId xContactId incognitoProfile = do +createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionRange -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> IO Contact +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case @@ -548,9 +630,9 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id) VALUES (?,?,?,?,?,?,?,?,?)" (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId) contactId <- insertedRowId db - activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId Nothing (Just userContactLinkId) customUserProfileId 0 createdAt + activeConn <- createConnection_ db userId ConnContact (Just contactId) agentConnId cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn - pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt} + pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn, viaGroup = Nothing, contactUsed = False, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False} getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName db User {userId} cName = @@ -564,15 +646,16 @@ getContact_ :: DB.Connection -> User -> Int64 -> Bool -> ExceptT StoreError IO C getContact_ db user@User {userId} contactId deleted = ExceptT . fmap join . firstRow (toContactOrError user) (SEContactNotFound contactId) $ DB.query - db + db [sql| SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id LEFT JOIN connections c ON c.contact_id = ct.contact_id @@ -620,7 +703,8 @@ getContactConnections db userId Contact {contactId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id WHERE c.user_id = ? AND ct.user_id = ? AND ct.contact_id = ? @@ -636,7 +720,8 @@ getConnectionById db User {userId} connId = ExceptT $ do db [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, - conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter + conn_status, conn_type, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, security_code, security_code_verified_at, auth_err_counter, + peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND connection_id = ? |] diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 49e149c7f0..b45da44a49 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -56,6 +56,8 @@ module Simplex.Chat.Store.Files startRcvInlineFT, xftpAcceptRcvFT, setRcvFileToReceive, + setFileCryptoArgs, + removeFileCryptoArgs, getRcvFilesToReceive, setRcvFTAgentDeleted, updateRcvFileStatus, @@ -83,19 +85,23 @@ import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime, nominalDay) import Data.Type.Equality import Database.SQLite.Simple (Only (..), (:.) (..)) -import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) +import Simplex.Chat.Messages +import Simplex.Chat.Messages.CIContent +import Simplex.Chat.Protocol import Simplex.Chat.Store.Direct import Simplex.Chat.Store.Messages import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Shared -import Simplex.Chat.Messages -import Simplex.Chat.Messages.CIContent -import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Util (week) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Protocol (SubscriptionMode (..)) getLiveSndFileTransfers :: DB.Connection -> User -> IO [SndFileTransfer] getLiveSndFileTransfers db User {userId} = do @@ -152,8 +158,8 @@ getPendingSndChunks db fileId connId = |] (fileId, connId) -createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> IO FileTransferMeta -createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize = do +createSndDirectFileTransfer :: DB.Connection -> UserId -> Contact -> FilePath -> FileInvitation -> Maybe ConnId -> Integer -> SubscriptionMode -> IO FileTransferMeta +createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitation {fileName, fileSize, fileInline} acId_ chunkSize subMode = do currentTs <- getCurrentTime DB.execute db @@ -161,7 +167,7 @@ createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitatio ((userId, contactId, fileName, filePath, fileSize, chunkSize) :. (fileInline, CIFSSndStored, FPSMP, currentTs, currentTs)) fileId <- insertedRowId db forM_ acId_ $ \acId -> do - Connection {connId} <- createSndFileConnection_ db userId fileId acId + Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode let fileStatus = FSNew DB.execute db @@ -169,10 +175,10 @@ createSndDirectFileTransfer db userId Contact {contactId} filePath FileInvitatio (fileId, fileStatus, fileInline, connId, currentTs, currentTs) pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} -createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> IO () -createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) = do +createSndDirectFTConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> SubscriptionMode -> IO () +createSndDirectFTConnection db user@User {userId} fileId (cmdId, acId) subMode = do currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId + Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode setCommandConnId db user cmdId connId DB.execute db @@ -189,10 +195,10 @@ createSndGroupFileTransfer db userId GroupInfo {groupId} filePath FileInvitation fileId <- insertedRowId db pure FileTransferMeta {fileId, xftpSndFile = Nothing, fileName, filePath, fileSize, fileInline, chunkSize, cancelled = False} -createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> IO () -createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} = do +createSndGroupFileTransferConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> GroupMember -> SubscriptionMode -> IO () +createSndGroupFileTransferConnection db user@User {userId} fileId (cmdId, acId) GroupMember {groupMemberId} subMode = do currentTs <- getCurrentTime - Connection {connId} <- createSndFileConnection_ db userId fileId acId + Connection {connId} <- createSndFileConnection_ db userId fileId acId subMode setCommandConnId db user cmdId connId DB.execute db @@ -257,14 +263,14 @@ getSndFTViaMsgDelivery db User {userId} Connection {connId, agentConnId} agentMs (\n -> SndFileTransfer {fileId, fileStatus, fileName, fileSize, chunkSize, filePath, fileDescrId, fileInline, groupMemberId, recipientDisplayName = n, connId, agentConnId}) <$> (contactName_ <|> memberName_) -createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> FilePath -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta -createSndFileTransferXFTP db User {userId} contactOrGroup filePath FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do +createSndFileTransferXFTP :: DB.Connection -> User -> ContactOrGroup -> CryptoFile -> FileInvitation -> AgentSndFileId -> Integer -> IO FileTransferMeta +createSndFileTransferXFTP db User {userId} contactOrGroup (CryptoFile filePath cryptoArgs) FileInvitation {fileName, fileSize} agentSndFileId chunkSize = do currentTs <- getCurrentTime - let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False} + let xftpSndFile = Just XFTPSndFile {agentSndFileId, privateSndFileDescr = Nothing, agentSndFileDeleted = False, cryptoArgs} DB.execute db - "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)" - (contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs)) + "INSERT INTO files (contact_id, group_id, user_id, file_name, file_path, file_crypto_key, file_crypto_nonce, file_size, chunk_size, agent_snd_file_id, ci_file_status, protocol, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)" + (contactAndGroupIds contactOrGroup :. (userId, fileName, filePath, CF.fileKey <$> cryptoArgs, CF.fileNonce <$> cryptoArgs, fileSize, chunkSize, agentSndFileId, CIFSSndStored, FPXFTP, currentTs, currentTs)) fileId <- insertedRowId db pure FileTransferMeta {fileId, xftpSndFile, fileName, filePath, fileSize, fileInline = Nothing, chunkSize, cancelled = False} @@ -418,10 +424,10 @@ getChatRefByFileId db User {userId} fileId = |] (userId, fileId) -createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> IO Connection -createSndFileConnection_ db userId fileId agentConnId = do +createSndFileConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection +createSndFileConnection_ db userId fileId agentConnId subMode = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId Nothing Nothing Nothing 0 currentTs + createConnection_ db userId ConnSndFile (Just fileId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -479,6 +485,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File currentTs <- liftIO getCurrentTime rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_ + -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do @@ -492,13 +499,14 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, rfdId, currentTs, currentTs) - pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing} + pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Nothing, cryptoArgs = Nothing} createRcvGroupFileTransfer :: DB.Connection -> UserId -> GroupMember -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localDisplayName = c} f@FileInvitation {fileName, fileSize, fileConnReq, fileInline, fileDescr} rcvFileInline chunkSize = do currentTs <- liftIO getCurrentTime rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (fileDescrId :: RcvFileDescr -> Int64) <$> rfd_ + -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do @@ -512,7 +520,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD db "INSERT INTO rcv_files (file_id, file_status, file_queue_info, file_inline, rcv_file_inline, group_member_id, file_descr_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (fileId, FSNew, fileConnReq, fileInline, rcvFileInline, groupMemberId, rfdId, currentTs, currentTs) - pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId} + pure RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = f, fileStatus = RFSNew, rcvFileInline, senderDisplayName = c, chunkSize, cancelled = False, grpMemberId = Just groupMemberId, cryptoArgs = Nothing} createRcvFD_ :: DB.Connection -> UserId -> UTCTime -> FileDescr -> ExceptT StoreError IO RcvFileDescr createRcvFD_ db userId currentTs FileDescr {fileDescrText, fileDescrPartNo, fileDescrComplete} = do @@ -600,7 +608,7 @@ getRcvFileTransfer db User {userId} fileId = do [sql| SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, - f.file_path, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id + f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN connections c ON r.file_id = c.rcv_file_id @@ -614,9 +622,9 @@ getRcvFileTransfer db User {userId} fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = case contactName_ <|> memberName_ of Nothing -> throwError $ SERcvFileInvalid fileId Just name -> do @@ -629,22 +637,23 @@ getRcvFileTransfer db User {userId} fileId = do where ft senderDisplayName fileStatus = let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} + cryptoArgs = CFArgs <$> fileKey <*> fileNonce xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ - in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId} + in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi_ = case (filePath_, connId_, agentConnId_) of (Just filePath, connId, agentConnId) -> pure $ Just RcvFileInfo {filePath, connId, agentConnId} _ -> pure Nothing cancelled = fromMaybe False cancelled_ -acceptRcvFileTransfer :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> ExceptT StoreError IO AChatItem -acceptRcvFileTransfer db user@User {userId} fileId (cmdId, acId) connStatus filePath = ExceptT $ do +acceptRcvFileTransfer :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem +acceptRcvFileTransfer db user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do currentTs <- getCurrentTime acceptRcvFT_ db user fileId filePath Nothing currentTs DB.execute db - "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" - (acId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs) + "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" + (acId, connStatus, ConnRcvFile, fileId, userId, currentTs, currentTs, subMode == SMOnlyCreate) connId <- insertedRowId db setCommandConnId db user cmdId connId runExceptT $ getChatItemByFileId db user fileId @@ -683,13 +692,26 @@ acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do "UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" (rcvFileInline, FSAccepted, currentTs, fileId) -setRcvFileToReceive :: DB.Connection -> FileTransferId -> IO () -setRcvFileToReceive db fileId = do +setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () +setRcvFileToReceive db fileId cfArgs_ = do currentTs <- getCurrentTime + DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) + forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs + +setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () +setFileCryptoArgs db fileId cfArgs = setFileCryptoArgs_ db fileId cfArgs =<< getCurrentTime + +setFileCryptoArgs_ :: DB.Connection -> FileTransferId -> CryptoFileArgs -> UTCTime -> IO () +setFileCryptoArgs_ db fileId (CFArgs key nonce) currentTs = DB.execute db - "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" - (currentTs, fileId) + "UPDATE files SET file_crypto_key = ?, file_crypto_nonce = ?, updated_at = ? WHERE file_id = ?" + (key, nonce, currentTs, fileId) + +removeFileCryptoArgs :: DB.Connection -> FileTransferId -> IO () +removeFileCryptoArgs db fileId = do + currentTs <- getCurrentTime + DB.execute db "UPDATE files SET file_crypto_key = NULL, file_crypto_nonce = NULL, updated_at = ? WHERE file_id = ?" (currentTs, fileId) getRcvFilesToReceive :: DB.Connection -> User -> IO [RcvFileTransfer] getRcvFilesToReceive db user@User {userId} = do @@ -842,15 +864,16 @@ getFileTransferMeta db User {userId} fileId = DB.query db [sql| - SELECT file_name, file_size, chunk_size, file_path, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled + SELECT file_name, file_size, chunk_size, file_path, file_crypto_key, file_crypto_nonce, file_inline, agent_snd_file_id, agent_snd_file_deleted, private_snd_file_descr, cancelled FROM files WHERE user_id = ? AND file_id = ? |] (userId, fileId) where - fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta - fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) = - let xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted}) <$> aSndFileId_ + fileTransferMeta :: (String, Integer, Integer, FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe AgentSndFileId, Bool, Maybe Text, Maybe Bool) -> FileTransferMeta + fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_) = + let cryptoArgs = CFArgs <$> fileKey <*> fileNonce + xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ in FileTransferMeta {fileId, xftpSndFile, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} getContactFileInfo :: DB.Connection -> User -> Contact -> IO [CIFileInfo] diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index aa36104670..f4ae569e19 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -34,16 +34,20 @@ module Simplex.Chat.Store.Groups updateGroupProfile, getGroupIdByName, getGroupMemberIdByName, + getActiveMembersByName, getGroupInfoByName, getGroupMember, getGroupMemberById, getGroupMembers, getGroupMembersForExpiration, + getGroupCurrentMembersCount, deleteGroupConnectionsAndFiles, deleteGroupItemsAndMembers, deleteGroup, getUserGroups, getUserGroupDetails, + getUserGroupsWithSummary, + getGroupSummary, getContactGroupPreferences, checkContactHasGroups, getGroupInvitation, @@ -70,16 +74,24 @@ module Simplex.Chat.Store.Groups getViaGroupMember, getViaGroupContact, getMatchingContacts, + getMatchingMemberContacts, createSentProbe, createSentProbeHash, - deleteSentProbe, matchReceivedProbe, matchReceivedProbeHash, matchSentProbe, mergeContactRecords, + updateMemberContact, updateGroupSettings, getXGrpMemIntroContDirect, getXGrpMemIntroContGroup, + getHostConnId, + createMemberContact, + getMemberContact, + setContactGrpInvSent, + createMemberContactInvited, + updateMemberContactInvited, + resetMemberContactFields, ) where @@ -87,11 +99,12 @@ import Control.Monad.Except import Crypto.Random (ChaChaDRG) import Data.Either (rights) import Data.Int (Int64) +import Data.List (sortOn) import Data.Maybe (fromMaybe, isNothing) +import Data.Ord (Down (..)) import Data.Text (Text) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), Query (..), (:.) (..)) -import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Messages import Simplex.Chat.Store.Direct @@ -100,8 +113,11 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C -import Simplex.Messaging.Util (eitherToMaybe) +import Simplex.Messaging.Protocol (SubscriptionMode (..)) +import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) +import Simplex.Messaging.Version import UnliftIO.STM type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe Bool, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime) :. GroupMemberRow @@ -130,8 +146,8 @@ toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just member Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, memberRole, memberCategory, memberStatus) :. (invitedById, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences)) toMaybeGroupMember _ _ = Nothing -createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> ExceptT StoreError IO () -createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole = +createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () +createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole subMode = checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do currentTs <- getCurrentTime DB.execute @@ -139,7 +155,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode getGroupLinkConnection :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO Connection getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = @@ -148,7 +164,8 @@ getGroupLinkConnection db User {userId} groupInfo@GroupInfo {groupId} = db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.group_id = ? @@ -229,7 +246,8 @@ getGroupAndMember db User {userId, userContactId} groupMemberId = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) JOIN groups g ON g.group_id = m.group_id @@ -398,13 +416,12 @@ deleteGroupConnectionsAndFiles db User {userId} GroupInfo {groupId} members = do DB.execute db "DELETE FROM files WHERE user_id = ? AND group_id = ?" (userId, groupId) deleteGroupItemsAndMembers :: DB.Connection -> User -> GroupInfo -> [GroupMember] -> IO () -deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = do +deleteGroupItemsAndMembers db user@User {userId} g@GroupInfo {groupId} members = do DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) void $ runExceptT cleanupHostGroupLinkConn_ -- to allow repeat connection via the same group link if one was used DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_id = ?" (userId, groupId) - forM_ members $ \m@GroupMember {memberProfile = LocalProfile {profileId}} -> do - cleanupMemberProfileAndName_ db user m - when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user profileId + forM_ members $ cleanupMemberProfileAndName_ db user + forM_ (incognitoMembershipProfile g) $ deleteUnusedIncognitoProfileById_ db user . localProfileId where cleanupHostGroupLinkConn_ = do hostId <- getHostMemberId_ db user groupId @@ -422,11 +439,11 @@ deleteGroupItemsAndMembers db user@User {userId} GroupInfo {groupId} members = d (userId, userId, hostId) deleteGroup :: DB.Connection -> User -> GroupInfo -> IO () -deleteGroup db user@User {userId} GroupInfo {groupId, localDisplayName, membership = membership@GroupMember {memberProfile = LocalProfile {profileId}}} = do +deleteGroup db user@User {userId} g@GroupInfo {groupId, localDisplayName} = do deleteGroupProfile_ db userId groupId DB.execute db "DELETE FROM groups WHERE user_id = ? AND group_id = ?" (userId, groupId) DB.execute db "DELETE FROM display_names WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) - when (memberIncognito membership) $ deleteUnusedIncognitoProfileById_ db user profileId + forM_ (incognitoMembershipProfile g) $ deleteUnusedIncognitoProfileById_ db user . localProfileId deleteGroupProfile_ :: DB.Connection -> UserId -> GroupId -> IO () deleteGroupProfile_ db userId groupId = @@ -447,8 +464,8 @@ getUserGroups db user@User {userId} = do groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ?" (Only userId) rights <$> mapM (runExceptT . getGroup db user) groupIds -getUserGroupDetails :: DB.Connection -> User -> IO [GroupInfo] -getUserGroupDetails db User {userId, userContactId} = +getUserGroupDetails :: DB.Connection -> User -> Maybe ContactId -> Maybe String -> IO [GroupInfo] +getUserGroupDetails db User {userId, userContactId} _contactId_ search_ = map (toGroupInfo userContactId) <$> DB.query db @@ -461,8 +478,36 @@ getUserGroupDetails db User {userId, userContactId} = JOIN group_members mu USING (group_id) JOIN contact_profiles pu ON pu.contact_profile_id = COALESCE(mu.member_profile_id, mu.contact_profile_id) WHERE g.user_id = ? AND mu.contact_id = ? + AND (gp.display_name LIKE '%' || ? || '%' OR gp.full_name LIKE '%' || ? || '%' OR gp.description LIKE '%' || ? || '%') |] - (userId, userContactId) + (userId, userContactId, search, search, search) + where + search = fromMaybe "" search_ + +getUserGroupsWithSummary :: DB.Connection -> User -> Maybe ContactId -> Maybe String -> IO [(GroupInfo, GroupSummary)] +getUserGroupsWithSummary db user _contactId_ search_ = + getUserGroupDetails db user _contactId_ search_ + >>= mapM (\g@GroupInfo {groupId} -> (g,) <$> getGroupSummary db user groupId) + +-- the statuses on non-current members should match memberCurrent' function +getGroupSummary :: DB.Connection -> User -> GroupId -> IO GroupSummary +getGroupSummary db User {userId} groupId = do + currentMembers_ <- + maybeFirstRow fromOnly $ + DB.query + db + [sql| + SELECT count (m.group_member_id) + FROM groups g + JOIN group_members m USING (group_id) + WHERE g.user_id = ? + AND g.group_id = ? + AND m.member_status != ? + AND m.member_status != ? + AND m.member_status != ? + |] + (userId, groupId, GSMemRemoved, GSMemLeft, GSMemInvited) + pure GroupSummary {currentMembers = fromMaybe 0 currentMembers_} getContactGroupPreferences :: DB.Connection -> User -> Contact -> IO [FullGroupPreferences] getContactGroupPreferences db User {userId} Contact {contactId} = do @@ -494,13 +539,14 @@ groupMemberQuery = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) LEFT JOIN connections c ON c.connection_id = ( SELECT max(cc.connection_id) FROM connections cc - where cc.user_id = ? AND cc.group_member_id = m.group_member_id + WHERE cc.user_id = ? AND cc.group_member_id = m.group_member_id ) |] @@ -548,6 +594,20 @@ toContactMember :: User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember toContactMember User {userContactId} (memberRow :. connRow) = (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection connRow} +getGroupCurrentMembersCount :: DB.Connection -> User -> GroupInfo -> IO Int +getGroupCurrentMembersCount db User {userId} GroupInfo {groupId} = do + statuses :: [GroupMemberStatus] <- + map fromOnly + <$> DB.query + db + [sql| + SELECT member_status + FROM group_members + WHERE group_id = ? AND user_id = ? + |] + (groupId, userId) + pure $ length $ filter memberCurrent' statuses + getGroupInvitation :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation getGroupInvitation db user groupId = getConnRec_ user >>= \case @@ -564,12 +624,12 @@ getGroupInvitation db user groupId = firstRow fromOnly (SEGroupNotFound groupId) $ DB.query db "SELECT g.inv_queue_info FROM groups g WHERE g.group_id = ? AND g.user_id = ?" (groupId, userId) -createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> ExceptT StoreError IO GroupMember -createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole agentConnId connRequest = +createNewContactMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> ConnId -> ConnReqInvitation -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createNewContactMember db gVar User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile, activeConn = Connection {peerChatVRange}} memberRole agentConnId connRequest subMode = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime member@GroupMember {groupMemberId} <- createMember_ (MemberId memId) createdAt - void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt + void $ createMemberConnection_ db userId groupMemberId agentConnId (fromJVersionRange peerChatVRange) Nothing 0 createdAt subMode pure member where createMember_ memberId createdAt = do @@ -604,13 +664,13 @@ createNewContactMember db gVar User {userId, userContactId} groupId Contact {con :. (userId, localDisplayName, contactId, localProfileId profile, connRequest, createdAt, createdAt) ) -createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> ExceptT StoreError IO () -createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) = +createNewContactMemberAsync :: DB.Connection -> TVar ChaChaDRG -> User -> GroupId -> Contact -> GroupMemberRole -> (CommandId, ConnId) -> VersionRange -> SubscriptionMode -> ExceptT StoreError IO () +createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Contact {contactId, localDisplayName, profile} memberRole (cmdId, agentConnId) peerChatVRange subMode = createWithRandomId gVar $ \memId -> do createdAt <- liftIO getCurrentTime insertMember_ (MemberId memId) createdAt groupMemberId <- liftIO $ insertedRowId db - Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 createdAt + Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 createdAt subMode setCommandConnId db user cmdId connId where insertMember_ memberId createdAt = @@ -626,30 +686,32 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} groupId Co :. (userId, localDisplayName, contactId, localProfileId profile, createdAt, createdAt) ) -getContactViaMember :: DB.Connection -> User -> GroupMember -> IO (Maybe Contact) +getContactViaMember :: DB.Connection -> User -> GroupMember -> ExceptT StoreError IO Contact getContactViaMember db user@User {userId} GroupMember {groupMemberId} = - maybeFirstRow (toContact user) $ - DB.query - db - [sql| - SELECT - -- Contact - ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, - -- Connection - c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter - FROM contacts ct - JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id - JOIN connections c ON c.connection_id = ( - SELECT max(cc.connection_id) - FROM connections cc - where cc.contact_id = ct.contact_id - ) - JOIN group_members m ON m.contact_id = ct.contact_id - WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0 - |] - (userId, groupMemberId) + ExceptT $ + firstRow (toContact user) (SEContactNotFoundByMemberId groupMemberId) $ + DB.query + db + [sql| + SELECT + -- Contact + ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, + -- Connection + c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version + FROM contacts ct + JOIN contact_profiles cp ON cp.contact_profile_id = ct.contact_profile_id + JOIN connections c ON c.connection_id = ( + SELECT max(cc.connection_id) + FROM connections cc + where cc.contact_id = ct.contact_id + ) + JOIN group_members m ON m.contact_id = ct.contact_id + WHERE ct.user_id = ? AND m.group_member_id = ? AND ct.deleted = 0 + |] + (userId, groupMemberId) setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO () setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do @@ -661,15 +723,15 @@ getMemberInvitation db User {userId} groupMemberId = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) -createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> IO () -createMemberConnection db userId GroupMember {groupMemberId} agentConnId = do +createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionRange -> SubscriptionMode -> IO () +createMemberConnection db userId GroupMember {groupMemberId} agentConnId peerChatVRange subMode = do currentTs <- getCurrentTime - void $ createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs + void $ createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode -createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> IO () -createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) = do +createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionRange -> SubscriptionMode -> IO () +createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) peerChatVRange subMode = do currentTs <- getCurrentTime - Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId Nothing 0 currentTs + Connection {connId} <- createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange Nothing 0 currentTs subMode setCommandConnId db user cmdId connId updateGroupMemberStatus :: DB.Connection -> UserId -> GroupMember -> GroupMemberStatus -> IO () @@ -689,25 +751,30 @@ updateGroupMemberStatusById db userId groupMemberId memStatus = do -- | add new member with profile createNewGroupMember :: DB.Connection -> User -> GroupInfo -> MemberInfo -> GroupMemberCategory -> GroupMemberStatus -> ExceptT StoreError IO GroupMember -createNewGroupMember db user@User {userId} gInfo memInfo@(MemberInfo _ _ Profile {displayName, fullName, image, contactLink, preferences}) memCategory memStatus = - ExceptT . withLocalDisplayName db userId displayName $ \localDisplayName -> do - currentTs <- getCurrentTime +createNewGroupMember db user gInfo memInfo memCategory memStatus = do + currentTs <- liftIO getCurrentTime + (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memInfo currentTs + let newMember = + NewGroupMember + { memInfo, + memCategory, + memStatus, + memInvitedBy = IBUnknown, + localDisplayName, + memContactId = Nothing, + memProfileId + } + liftIO $ createNewMember_ db user gInfo newMember currentTs + +createNewMemberProfile_ :: DB.Connection -> User -> MemberInfo -> UTCTime -> ExceptT StoreError IO (Text, ProfileId) +createNewMemberProfile_ db User {userId} (MemberInfo _ _ _ Profile {displayName, fullName, image, contactLink, preferences}) createdAt = + ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute db "INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)" - (displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs) - memProfileId <- insertedRowId db - let newMember = - NewGroupMember - { memInfo, - memCategory, - memStatus, - memInvitedBy = IBUnknown, - localDisplayName, - memContactId = Nothing, - memProfileId - } - Right <$> createNewMember_ db user gInfo newMember currentTs + (displayName, fullName, image, contactLink, userId, preferences, createdAt, createdAt) + profileId <- insertedRowId db + pure $ Right (ldn, profileId) createNewMember_ :: DB.Connection -> User -> GroupInfo -> NewGroupMember -> UTCTime -> IO GroupMember createNewMember_ @@ -715,7 +782,7 @@ createNewMember_ User {userId, userContactId} GroupInfo {groupId} NewGroupMember - { memInfo = MemberInfo memberId memberRole memberProfile, + { memInfo = MemberInfo memberId memberRole _ memberProfile, memCategory = memberCategory, memStatus = memberStatus, memInvitedBy = invitedBy, @@ -743,12 +810,12 @@ checkGroupMemberHasItems db User {userId} GroupMember {groupMemberId, groupId} = maybeFirstRow fromOnly $ DB.query db "SELECT chat_item_id FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ? LIMIT 1" (userId, groupId, groupMemberId) deleteGroupMember :: DB.Connection -> User -> GroupMember -> IO () -deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, memberProfile = LocalProfile {profileId}} = do +deleteGroupMember db user@User {userId} m@GroupMember {groupMemberId, groupId, memberProfile} = do deleteGroupMemberConnection db user m DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ? AND group_member_id = ?" (userId, groupId, groupMemberId) DB.execute db "DELETE FROM group_members WHERE user_id = ? AND group_member_id = ?" (userId, groupMemberId) cleanupMemberProfileAndName_ db user m - when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user profileId + when (memberIncognito m) $ deleteUnusedIncognitoProfileById_ db user $ localProfileId memberProfile cleanupMemberProfileAndName_ :: DB.Connection -> User -> GroupMember -> IO () cleanupMemberProfileAndName_ db User {userId} GroupMember {groupMemberId, memberContactId, memberContactProfileId, localDisplayName} = @@ -859,43 +926,41 @@ getIntroduction_ db reMember toMember = ExceptT $ do where toIntro :: [(Int64, Maybe ConnReqInvitation, Maybe ConnReqInvitation, GroupMemberIntroStatus)] -> Either StoreError GroupMemberIntro toIntro [(introId, groupConnReq, directConnReq, introStatus)] = - let introInvitation = IntroInvitation <$> groupConnReq <*> directConnReq + let introInvitation = IntroInvitation <$> groupConnReq <*> pure directConnReq in Right GroupMemberIntro {introId, reMember, toMember, introStatus, introInvitation} toIntro _ = Left SEIntroNotFound -createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> ExceptT StoreError IO GroupMember -createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberProfile) (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do - let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn +createIntroReMember :: DB.Connection -> User -> GroupInfo -> GroupMember -> MemberInfo -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> ExceptT StoreError IO GroupMember +createIntroReMember db user@User {userId} gInfo@GroupInfo {groupId} _host@GroupMember {memberContactId, activeConn} memInfo@(MemberInfo _ _ memberChatVRange memberProfile) (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do + let mcvr = maybe chatInitialVRange fromChatVRange memberChatVRange + cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- liftIO getCurrentTime - Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId memberContactId Nothing customUserProfileId cLevel currentTs - liftIO $ setCommandConnId db user directCmdId directConnId - (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing + newMember <- case directConnIds of + Just (directCmdId, directAgentConnId) -> do + Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode + liftIO $ setCommandConnId db user directCmdId directConnId + (localDisplayName, contactId, memProfileId) <- createContact_ db userId directConnId memberProfile "" (Just groupId) currentTs Nothing + pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Just contactId, memProfileId} + Nothing -> do + (localDisplayName, memProfileId) <- createNewMemberProfile_ db user memInfo currentTs + pure $ NewGroupMember {memInfo, memCategory = GCPreMember, memStatus = GSMemIntroduced, memInvitedBy = IBUnknown, localDisplayName, memContactId = Nothing, memProfileId} liftIO $ do - let newMember = - NewGroupMember - { memInfo, - memCategory = GCPreMember, - memStatus = GSMemIntroduced, - memInvitedBy = IBUnknown, - localDisplayName, - memContactId = Just contactId, - memProfileId - } member <- createNewMember_ db user gInfo newMember currentTs - conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId memberContactId cLevel currentTs + conn@Connection {connId = groupConnId} <- createMemberConnection_ db userId (groupMemberId' member) groupAgentConnId mcvr memberContactId cLevel currentTs subMode liftIO $ setCommandConnId db user groupCmdId groupConnId pure (member :: GroupMember) {activeConn = Just conn} -createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> (CommandId, ConnId) -> (CommandId, ConnId) -> Maybe ProfileId -> IO () -createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} (groupCmdId, groupAgentConnId) (directCmdId, directAgentConnId) customUserProfileId = do +createIntroToMemberContact :: DB.Connection -> User -> GroupMember -> GroupMember -> VersionRange -> (CommandId, ConnId) -> Maybe (CommandId, ConnId) -> Maybe ProfileId -> SubscriptionMode -> IO () +createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = viaContactId, activeConn} _to@GroupMember {groupMemberId, localDisplayName} mcvr (groupCmdId, groupAgentConnId) directConnIds customUserProfileId subMode = do let cLevel = 1 + maybe 0 (connLevel :: Connection -> Int) activeConn currentTs <- getCurrentTime - Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId viaContactId cLevel currentTs + Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId mcvr viaContactId cLevel currentTs subMode setCommandConnId db user groupCmdId groupConnId - Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId viaContactId Nothing customUserProfileId cLevel currentTs - setCommandConnId db user directCmdId directConnId - contactId <- createMemberContact_ directConnId currentTs - updateMember_ contactId currentTs + forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do + Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode + setCommandConnId db user directCmdId directConnId + contactId <- createMemberContact_ directConnId currentTs + updateMember_ contactId currentTs where createMemberContact_ :: Int64 -> UTCTime -> IO Int64 createMemberContact_ connId ts = do @@ -922,8 +987,8 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = |] [":contact_id" := contactId, ":updated_at" := ts, ":group_member_id" := groupMemberId] -createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> Maybe Int64 -> Int -> UTCTime -> IO Connection -createMemberConnection_ db userId groupMemberId agentConnId viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId viaContact Nothing Nothing +createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionRange -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection +createMemberConnection_ db userId groupMemberId agentConnId peerChatVRange viaContact = createConnection_ db userId ConnMember (Just groupMemberId) agentConnId peerChatVRange viaContact Nothing Nothing getViaGroupMember :: DB.Connection -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) getViaGroupMember db User {userId, userContactId} Contact {contactId} = @@ -943,7 +1008,8 @@ getViaGroupMember db User {userId, userContactId} Contact {contactId} = m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -974,9 +1040,10 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = [sql| SELECT ct.contact_id, ct.contact_profile_id, ct.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, ct.via_group, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, + p.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id JOIN connections c ON c.connection_id = ( @@ -990,13 +1057,13 @@ getViaGroupContact db user@User {userId} GroupMember {groupMemberId} = |] (userId, groupMemberId) where - toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime)) :. ConnectionRow -> Contact - toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = + toContact' :: ((ContactId, ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)) :. ConnectionRow -> Contact + toContact' (((contactId, profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite} activeConn = toConnection connRow mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} updateGroupProfile :: DB.Connection -> User -> GroupInfo -> GroupProfile -> ExceptT StoreError IO GroupInfo updateGroupProfile db User {userId} g@GroupInfo {groupId, localDisplayName, groupProfile = GroupProfile {displayName}} p'@GroupProfile {displayName = newName, fullName, description, image, groupPreferences} @@ -1063,112 +1130,160 @@ getGroupMemberIdByName db User {userId} groupId groupMemberName = ExceptT . firstRow fromOnly (SEGroupMemberNameNotFound groupId groupMemberName) $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ?" (userId, groupId, groupMemberName) +getActiveMembersByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] +getActiveMembersByName db user@User {userId} groupMemberName = do + groupMemberIds :: [(GroupId, GroupMemberId)] <- + liftIO $ + DB.query + db + [sql| + SELECT group_id, group_member_id + FROM group_members + WHERE user_id = ? AND local_display_name = ? + AND member_status IN (?,?) AND member_category != ? + |] + (userId, groupMemberName, GSMemConnected, GSMemComplete, GCUserMember) + possibleMembers <- forM groupMemberIds $ \(groupId, groupMemberId) -> do + groupInfo <- getGroupInfo db user groupId + groupMember <- getGroupMember db user groupId groupMemberId + pure (groupInfo, groupMember) + pure $ sortOn (Down . ts . fst) possibleMembers + where + ts GroupInfo {chatTs, updatedAt} = fromMaybe updatedAt chatTs + getMatchingContacts :: DB.Connection -> User -> Contact -> IO [Contact] getMatchingContacts db user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, image}} = do contactIds <- - map fromOnly - <$> DB.query - db - [sql| - SELECT ct.contact_id - FROM contacts ct - JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id - WHERE ct.user_id = ? AND ct.contact_id != ? - AND ct.deleted = 0 - AND p.display_name = ? AND p.full_name = ? - AND ((p.image IS NULL AND ? IS NULL) OR p.image = ?) - |] - (userId, contactId, displayName, fullName, image, image) + map fromOnly <$> case image of + Just img -> DB.query db (q <> " AND p.image = ?") (userId, contactId, displayName, fullName, img) + Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, contactId, displayName, fullName) rights <$> mapM (runExceptT . getContact db user) contactIds + where + -- this query is different from one in getMatchingMemberContacts + -- it checks that it's not the same contact + q = + [sql| + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id + WHERE ct.user_id = ? AND ct.contact_id != ? + AND ct.deleted = 0 + AND p.display_name = ? AND p.full_name = ? + |] -createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> Contact -> ExceptT StoreError IO (Probe, Int64) -createSentProbe db gVar userId _to@Contact {contactId} = +getMatchingMemberContacts :: DB.Connection -> User -> GroupMember -> IO [Contact] +getMatchingMemberContacts _ _ GroupMember {memberContactId = Just _} = pure [] +getMatchingMemberContacts db user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, image}} = do + contactIds <- + map fromOnly <$> case image of + Just img -> DB.query db (q <> " AND p.image = ?") (userId, displayName, fullName, img) + Nothing -> DB.query db (q <> " AND p.image is NULL") (userId, displayName, fullName) + rights <$> mapM (runExceptT . getContact db user) contactIds + where + q = + [sql| + SELECT ct.contact_id + FROM contacts ct + JOIN contact_profiles p ON ct.contact_profile_id = p.contact_profile_id + WHERE ct.user_id = ? + AND ct.deleted = 0 + AND p.display_name = ? AND p.full_name = ? + |] + +createSentProbe :: DB.Connection -> TVar ChaChaDRG -> UserId -> ContactOrGroupMember -> ExceptT StoreError IO (Probe, Int64) +createSentProbe db gVar userId to = createWithRandomBytes 32 gVar $ \probe -> do currentTs <- getCurrentTime + let (ctId, gmId) = contactOrGroupMemberIds to DB.execute db - "INSERT INTO sent_probes (contact_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (contactId, probe, userId, currentTs, currentTs) - (Probe probe,) <$> insertedRowId db + "INSERT INTO sent_probes (contact_id, group_member_id, probe, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (ctId, gmId, probe, userId, currentTs, currentTs) + (Probe probe,) <$> insertedRowId db -createSentProbeHash :: DB.Connection -> UserId -> Int64 -> Contact -> IO () -createSentProbeHash db userId probeId _to@Contact {contactId} = do +createSentProbeHash :: DB.Connection -> UserId -> Int64 -> ContactOrGroupMember -> IO () +createSentProbeHash db userId probeId to = do currentTs <- getCurrentTime + let (ctId, gmId) = contactOrGroupMemberIds to DB.execute db - "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (probeId, contactId, userId, currentTs, currentTs) + "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (probeId, ctId, gmId, userId, currentTs, currentTs) -deleteSentProbe :: DB.Connection -> UserId -> Int64 -> IO () -deleteSentProbe db userId probeId = - DB.execute - db - "DELETE FROM sent_probes WHERE user_id = ? AND sent_probe_id = ?" - (userId, probeId) - -matchReceivedProbe :: DB.Connection -> User -> Contact -> Probe -> IO (Maybe Contact) -matchReceivedProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = do +matchReceivedProbe :: DB.Connection -> User -> ContactOrGroupMember -> Probe -> IO (Maybe ContactOrGroupMember) +matchReceivedProbe db user@User {userId} from (Probe probe) = do let probeHash = C.sha256Hash probe - contactIds <- - map fromOnly - <$> DB.query + cgmIds <- + maybeFirstRow id $ + DB.query db [sql| - SELECT c.contact_id - FROM contacts c - JOIN received_probes r ON r.contact_id = c.contact_id - WHERE c.user_id = ? AND c.deleted = 0 AND r.probe_hash = ? AND r.probe IS NULL + SELECT r.contact_id, g.group_id, r.group_member_id + FROM received_probes r + LEFT JOIN contacts c ON r.contact_id = c.contact_id AND c.deleted = 0 + LEFT JOIN group_members m ON r.group_member_id = m.group_member_id + LEFT JOIN groups g ON g.group_id = m.group_id + WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NULL |] (userId, probeHash) currentTs <- getCurrentTime + let (ctId, gmId) = contactOrGroupMemberIds from DB.execute db - "INSERT INTO received_probes (contact_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" - (contactId, probe, probeHash, userId, currentTs, currentTs) - case contactIds of - [] -> pure Nothing - cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId) + "INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" + (ctId, gmId, probe, probeHash, userId, currentTs, currentTs) + pure cgmIds $>>= getContactOrGroupMember_ db user -matchReceivedProbeHash :: DB.Connection -> User -> Contact -> ProbeHash -> IO (Maybe (Contact, Probe)) -matchReceivedProbeHash db user@User {userId} _from@Contact {contactId} (ProbeHash probeHash) = do - namesAndProbes <- - DB.query - db - [sql| - SELECT c.contact_id, r.probe - FROM contacts c - JOIN received_probes r ON r.contact_id = c.contact_id - WHERE c.user_id = ? AND c.deleted = 0 AND r.probe_hash = ? AND r.probe IS NOT NULL - |] - (userId, probeHash) - currentTs <- getCurrentTime - DB.execute - db - "INSERT INTO received_probes (contact_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?)" - (contactId, probeHash, userId, currentTs, currentTs) - case namesAndProbes of - [] -> pure Nothing - (cId, probe) : _ -> - either (const Nothing) (Just . (,Probe probe)) - <$> runExceptT (getContact db user cId) - -matchSentProbe :: DB.Connection -> User -> Contact -> Probe -> IO (Maybe Contact) -matchSentProbe db user@User {userId} _from@Contact {contactId} (Probe probe) = do - contactIds <- - map fromOnly - <$> DB.query +matchReceivedProbeHash :: DB.Connection -> User -> ContactOrGroupMember -> ProbeHash -> IO (Maybe (ContactOrGroupMember, Probe)) +matchReceivedProbeHash db user@User {userId} from (ProbeHash probeHash) = do + probeIds <- + maybeFirstRow id $ + DB.query db [sql| - SELECT c.contact_id - FROM contacts c - JOIN sent_probes s ON s.contact_id = c.contact_id - JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id - WHERE c.user_id = ? AND c.deleted = 0 AND s.probe = ? AND h.contact_id = ? + SELECT r.probe, r.contact_id, g.group_id, r.group_member_id + FROM received_probes r + LEFT JOIN contacts c ON r.contact_id = c.contact_id AND c.deleted = 0 + LEFT JOIN group_members m ON r.group_member_id = m.group_member_id + LEFT JOIN groups g ON g.group_id = m.group_id + WHERE r.user_id = ? AND r.probe_hash = ? AND r.probe IS NOT NULL |] - (userId, probe, contactId) - case contactIds of - [] -> pure Nothing - cId : _ -> eitherToMaybe <$> runExceptT (getContact db user cId) + (userId, probeHash) + currentTs <- getCurrentTime + let (ctId, gmId) = contactOrGroupMemberIds from + DB.execute + db + "INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" + (ctId, gmId, probeHash, userId, currentTs, currentTs) + pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrGroupMember_ db user cgmIds + +matchSentProbe :: DB.Connection -> User -> ContactOrGroupMember -> Probe -> IO (Maybe ContactOrGroupMember) +matchSentProbe db user@User {userId} _from (Probe probe) = + cgmIds $>>= getContactOrGroupMember_ db user + where + (ctId, gmId) = contactOrGroupMemberIds _from + cgmIds = + maybeFirstRow id $ + DB.query + db + [sql| + SELECT s.contact_id, g.group_id, s.group_member_id + FROM sent_probes s + LEFT JOIN contacts c ON s.contact_id = c.contact_id AND c.deleted = 0 + LEFT JOIN group_members m ON s.group_member_id = m.group_member_id + LEFT JOIN groups g ON g.group_id = m.group_id + JOIN sent_probe_hashes h ON h.sent_probe_id = s.sent_probe_id + WHERE s.user_id = ? AND s.probe = ? + AND (h.contact_id = ? OR h.group_member_id = ?) + |] + (userId, probe, ctId, gmId) + +getContactOrGroupMember_ :: DB.Connection -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrGroupMember) +getContactOrGroupMember_ db user ids = + fmap eitherToMaybe . runExceptT $ case ids of + (Just ctId, _, _) -> CGMContact <$> getContact db user ctId + (_, Just gId, Just gmId) -> CGMGroupMember <$> getGroupInfo db user gId <*> getGroupMember db user gId gmId + _ -> throwError $ SEInternalError "" mergeContactRecords :: DB.Connection -> UserId -> Contact -> Contact -> IO () mergeContactRecords db userId ct1 ct2 = do @@ -1216,7 +1331,7 @@ mergeContactRecords db userId ct1 ct2 = do ] deleteContactProfile_ db userId fromContactId DB.execute db "DELETE FROM contacts WHERE contact_id = ? AND user_id = ?" (fromContactId, userId) - DB.execute db "DELETE FROM display_names WHERE local_display_name = ? AND user_id = ?" (localDisplayName, userId) + deleteUnusedDisplayName_ db userId localDisplayName where toFromContacts :: Contact -> Contact -> (Contact, Contact) toFromContacts c1 c2 @@ -1229,6 +1344,64 @@ mergeContactRecords db userId ct1 ct2 = do d2 = directOrUsed c2 ctCreatedAt Contact {createdAt} = createdAt +updateMemberContact :: DB.Connection -> User -> Contact -> GroupMember -> IO () +updateMemberContact + db + User {userId} + Contact {contactId, localDisplayName, profile = LocalProfile {profileId}} + GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}} = do + -- TODO possibly, we should update profiles and local_display_names of all members linked to the same remote user, + -- once we decide on how we identify it, either based on shared contact_profile_id or on local_display_name + currentTs <- getCurrentTime + DB.execute + db + [sql| + UPDATE group_members + SET contact_id = ?, local_display_name = ?, contact_profile_id = ?, updated_at = ? + WHERE user_id = ? AND group_id = ? AND group_member_id = ? + |] + (contactId, localDisplayName, profileId, currentTs, userId, groupId, groupMemberId) + when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId + when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN + +deleteUnusedDisplayName_ :: DB.Connection -> UserId -> ContactName -> IO () +deleteUnusedDisplayName_ db userId localDisplayName = + DB.executeNamed + db + [sql| + DELETE FROM display_names + WHERE user_id = :user_id AND local_display_name = :local_display_name + AND 1 NOT IN ( + SELECT 1 FROM users + WHERE local_display_name = :local_display_name LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contacts + WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM groups + WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM group_members + WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM user_contact_links + WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contact_requests + WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + ) + AND 1 NOT IN ( + SELECT 1 FROM contact_requests + WHERE user_id = :user_id AND local_display_name = :local_display_name LIMIT 1 + ) + |] + [":user_id" := userId, ":local_display_name" := localDisplayName] + updateGroupSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () updateGroupSettings db User {userId} groupId ChatSettings {enableNtfs, sendRcpts, favorite} = DB.execute db "UPDATE groups SET enable_ntfs = ?, send_rcpts = ?, favorite = ? WHERE user_id = ? AND group_id = ?" (enableNtfs, sendRcpts, favorite, userId, groupId) @@ -1292,3 +1465,160 @@ getXGrpMemIntroContGroup db User {userId} GroupMember {groupMemberId} = do toCont (hostConnId, connReq_) = case connReq_ of Just connReq -> Just (hostConnId, connReq) _ -> Nothing + +getHostConnId :: DB.Connection -> User -> GroupId -> ExceptT StoreError IO GroupMemberId +getHostConnId db user@User {userId} groupId = do + hostMemberId <- getHostMemberId_ db user groupId + ExceptT . firstRow fromOnly (SEConnectionNotFoundByMemberId hostMemberId) $ + DB.query db "SELECT connection_id FROM connections WHERE user_id = ? AND group_member_id = ?" (userId, hostMemberId) + +createMemberContact :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> GroupInfo -> GroupMember -> Connection -> SubscriptionMode -> IO Contact +createMemberContact + db + user@User {userId, profile = LocalProfile {preferences}} + acId + cReq + gInfo + GroupMember {groupMemberId, localDisplayName, memberProfile, memberContactProfileId} + Connection {connLevel, peerChatVRange = peerChatVRange@(JVersionRange (VersionRange minV maxV))} + subMode = do + currentTs <- getCurrentTime + let incognitoProfile = incognitoMembershipProfile gInfo + customUserProfileId = localProfileId <$> incognitoProfile + userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences + DB.execute + db + [sql| + INSERT INTO contacts ( + user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, contact_used, + contact_group_member_id, contact_grp_inv_sent, created_at, updated_at, chat_ts + ) VALUES (?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, localDisplayName, memberContactProfileId, True, userPreferences, True) + :. (groupMemberId, False, currentTs, currentTs, currentTs) + ) + contactId <- insertedRowId db + DB.execute + db + "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE group_member_id = ?" + (contactId, currentTs, groupMemberId) + DB.execute + db + [sql| + INSERT INTO connections ( + user_id, agent_conn_id, conn_req_inv, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id, + peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, acId, cReq, connLevel, ConnNew, ConnContact, contactId, customUserProfileId) + :. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate) + ) + connId <- insertedRowId db + let ctConn = Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} + mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False} + +getMemberContact :: DB.Connection -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) +getMemberContact db user contactId = do + ct <- getContact db user contactId + let Contact {contactGroupMemberId, activeConn = Connection {connId}} = ct + cReq <- getConnReqInv db connId + case contactGroupMemberId of + Just groupMemberId -> do + m@GroupMember {groupId} <- getGroupMemberById db user groupMemberId + g <- getGroupInfo db user groupId + pure (g, m, ct, cReq) + _ -> + throwError $ SEMemberContactGroupMemberNotFound contactId + +setContactGrpInvSent :: DB.Connection -> Contact -> Bool -> IO () +setContactGrpInvSent db Contact {contactId} xGrpDirectInvSent = do + currentTs <- getCurrentTime + DB.execute + db + "UPDATE contacts SET contact_grp_inv_sent = ?, updated_at = ? WHERE contact_id = ?" + (xGrpDirectInvSent, currentTs, contactId) + +createMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> GroupMember -> Connection -> SubscriptionMode -> IO (Contact, GroupMember) +createMemberContactInvited + db + user@User {userId, profile = LocalProfile {preferences}} + connIds + gInfo + m@GroupMember {groupMemberId, localDisplayName = memberLDN, memberProfile, memberContactProfileId} + mConn + subMode = do + currentTs <- liftIO getCurrentTime + let userPreferences = fromMaybe emptyChatPrefs $ incognitoMembershipProfile gInfo >> preferences + contactId <- createContactUpdateMember currentTs userPreferences + ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode + let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn + mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = ctConn, viaGroup = Nothing, contactUsed = True, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} + m' = m {memberContactId = Just contactId} + pure (mCt', m') + where + createContactUpdateMember :: UTCTime -> Preferences -> IO ContactId + createContactUpdateMember currentTs userPreferences = do + DB.execute + db + [sql| + INSERT INTO contacts ( + user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, contact_used, + created_at, updated_at, chat_ts + ) VALUES (?,?,?,?,?,?,?,?,?) + |] + ( (userId, memberLDN, memberContactProfileId, True, userPreferences, True) + :. (currentTs, currentTs, currentTs) + ) + contactId <- insertedRowId db + DB.execute + db + "UPDATE group_members SET contact_id = ?, updated_at = ? WHERE group_member_id = ?" + (contactId, currentTs, groupMemberId) + pure contactId + +updateMemberContactInvited :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> Connection -> Contact -> SubscriptionMode -> IO Contact +updateMemberContactInvited db user connIds gInfo mConn ct@Contact {contactId, activeConn = oldContactConn} subMode = do + updateConnectionStatus db oldContactConn ConnDeleted + activeConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode + ct' <- resetMemberContactFields db ct + pure (ct' :: Contact) {activeConn} + +resetMemberContactFields :: DB.Connection -> Contact -> IO Contact +resetMemberContactFields db ct@Contact {contactId} = do + currentTs <- liftIO getCurrentTime + DB.execute + db + [sql| + UPDATE contacts + SET contact_group_member_id = NULL, contact_grp_inv_sent = 0, updated_at = ? + WHERE contact_id = ? + |] + (currentTs, contactId) + pure ct {contactGroupMemberId = Nothing, contactGrpInvSent = False, updatedAt = currentTs} + +createMemberContactConn_ :: DB.Connection -> User -> (CommandId, ConnId) -> GroupInfo -> Connection -> ContactId -> SubscriptionMode -> IO Connection +createMemberContactConn_ + db + user@User {userId} + (cmdId, acId) + gInfo + _memberConn@Connection {connLevel, peerChatVRange = peerChatVRange@(JVersionRange (VersionRange minV maxV))} + contactId + subMode = do + currentTs <- liftIO getCurrentTime + let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo + DB.execute + db + [sql| + INSERT INTO connections ( + user_id, agent_conn_id, conn_level, conn_status, conn_type, contact_id, custom_user_profile_id, + peer_chat_min_version, peer_chat_max_version, created_at, updated_at, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) + |] + ( (userId, acId, connLevel, ConnNew, ConnContact, contactId, customUserProfileId) + :. (minV, maxV, currentTs, currentTs, subMode == SMOnlyCreate) + ) + connId <- insertedRowId db + setCommandConnId db user cmdId connId + pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connType = ConnContact, entityId = Just contactId, viaContact = Nothing, viaUserContactLink = Nothing, viaGroupLink = False, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 29e6e2abc3..5f64b9e390 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -13,7 +13,6 @@ module Simplex.Chat.Store.Messages ( getContactConnIds_, getDirectChatReactions_, - toDirectChatItem, -- * Message and chat item functions deleteContactCIs, @@ -44,6 +43,7 @@ module Simplex.Chat.Store.Messages createChatItemVersion, deleteDirectChatItem, markDirectChatItemDeleted, + updateGroupChatItemStatus, updateGroupChatItem, deleteGroupChatItem, updateGroupChatItemModerated, @@ -69,6 +69,7 @@ module Simplex.Chat.Store.Messages getGroupChatItem, getGroupChatItemBySharedMsgId, getGroupMemberCIBySharedMsgId, + getGroupChatItemByAgentMsgId, getGroupMemberChatItemLast, getDirectChatItemIdByText, getDirectChatItemIdByText', @@ -87,6 +88,11 @@ module Simplex.Chat.Store.Messages createCIModeration, getCIModeration, deleteCIModeration, + createGroupSndStatus, + getGroupSndStatus, + updateGroupSndStatus, + getGroupSndStatuses, + getGroupSndStatusCounts, ) where @@ -103,7 +109,6 @@ import Data.Text (Text) import Data.Time (addUTCTime) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) -import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) import Simplex.Chat.Markdown import Simplex.Chat.Messages @@ -115,6 +120,9 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, firstRow', maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Util (eitherToMaybe) import UnliftIO.STM @@ -467,16 +475,17 @@ getDirectChatPreviews_ db user@User {userId} = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version, -- ChatStats COALESCE(ChatStats.UnreadCount, 0), COALESCE(ChatStats.MinUnread, 0), ct.unread_chat, -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM contacts ct @@ -541,7 +550,7 @@ getGroupChatPreviews_ db User {userId, userContactId} = do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- Maybe GroupMember - sender m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, @@ -601,7 +610,8 @@ getContactRequestChatPreviews_ db User {userId} = [sql| SELECT cr.contact_request_id, cr.local_display_name, cr.agent_invitation_id, cr.user_contact_link_id, - c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at + c.agent_conn_id, cr.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, cr.xcontact_id, p.preferences, cr.created_at, cr.updated_at, + cr.peer_chat_min_version, cr.peer_chat_max_version FROM contact_requests cr JOIN connections c ON c.user_contact_link_id = cr.user_contact_link_id JOIN contact_profiles p ON p.contact_profile_id = cr.contact_profile_id @@ -662,7 +672,7 @@ getDirectChatItemsLast db User {userId} contactId count search = ExceptT $ do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -691,7 +701,7 @@ getDirectChatAfter_ db User {userId} ct@Contact {contactId} afterChatItemId coun -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -721,7 +731,7 @@ getDirectChatBefore_ db User {userId} ct@Contact {contactId} beforeChatItemId co -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -943,7 +953,7 @@ type ChatStatsRow = (Int, ChatItemId, Bool) toChatStats :: ChatStatsRow -> ChatStats toChatStats (unreadCount, minUnreadItemId, unreadChat) = ChatStats {unreadCount, minUnreadItemId, unreadChat} -type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe ACIFileStatus, Maybe FileProtocol) +type MaybeCIFIleRow = (Maybe Int64, Maybe String, Maybe Integer, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe ACIFileStatus, Maybe FileProtocol) type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) @@ -964,7 +974,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir -- this function can be changed so it never fails, not only avoid failure on invalid json toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) -toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. quoteRow) = +toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -981,7 +991,10 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) maybeCIFile fileStatus = case (fileId_, fileName_, fileSize_, fileProtocol_) of - (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol} + (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> + let cfArgs = CFArgs <$> fileKey <*> fileNonce + fileSource = (`CryptoFile` cfArgs) <$> filePath + in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} _ -> Nothing cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTDirect d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTDirect cItem d chatDir ciStatus content file = @@ -1014,7 +1027,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction -- this function can be changed so it never fails, not only avoid failure on invalid json toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) -toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do +toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do chatItem $ fromRight invalid $ dbParseACIContent itemContentText where member_ = toMaybeGroupMember userContactId memberRow_ @@ -1034,7 +1047,10 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, maybeCIFile :: CIFileStatus d -> Maybe (CIFile d) maybeCIFile fileStatus = case (fileId_, fileName_, fileSize_, fileProtocol_) of - (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> Just CIFile {fileId, fileName, fileSize, filePath, fileStatus, fileProtocol} + (Just fileId, Just fileName, Just fileSize, Just fileProtocol) -> + let cfArgs = CFArgs <$> fileKey <*> fileNonce + fileSource = (`CryptoFile` cfArgs) <$> filePath + in Just CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol} _ -> Nothing cItem :: MsgDirectionI d => SMsgDirection d -> CIDirection 'CTGroup d -> CIStatus d -> CIContent d -> Maybe (CIFile d) -> CChatItem 'CTGroup cItem d chatDir ciStatus content file = @@ -1134,7 +1150,7 @@ updateDirectChatItemStatus db user@User {userId} contactId itemId itemStatus = d correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) updateDirectChatItem db user contactId itemId newContent live msgId_ = do ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_ @@ -1142,7 +1158,7 @@ updateDirectChatItem db user contactId itemId newContent live msgId_ = do correctDir :: CChatItem c -> Either StoreError (ChatItem c d) correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem' :: forall d. (MsgDirectionI d) => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) +updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do currentTs <- liftIO getCurrentTime let ci' = updatedChatItem ci newContent live currentTs @@ -1287,7 +1303,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- DirectQuote ri.chat_item_id, i.quoted_shared_msg_id, i.quoted_sent_at, i.quoted_content, i.quoted_sent FROM chat_items i @@ -1325,6 +1341,16 @@ getDirectChatItemIdByText' db User {userId} contactId msg = |] (userId, contactId, msg <> "%") +updateGroupChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> GroupId -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTGroup d) +updateGroupChatItemStatus db user@User {userId} groupId itemId itemStatus = do + ci <- liftEither . correctDir =<< getGroupChatItem db user groupId itemId + currentTs <- liftIO getCurrentTime + liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, groupId, itemId) + pure ci {meta = (meta ci) {itemStatus}} + where + correctDir :: CChatItem c -> Either StoreError (ChatItem c d) + correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci + updateGroupChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) updateGroupChatItem db user groupId ci newContent live msgId_ = do currentTs <- liftIO getCurrentTime @@ -1434,6 +1460,11 @@ getGroupMemberCIBySharedMsgId db user@User {userId} groupId memberId sharedMsgId (GCUserMember, userId, groupId, memberId, sharedMsgId) getGroupChatItem db user groupId itemId +getGroupChatItemByAgentMsgId :: DB.Connection -> User -> GroupId -> Int64 -> AgentMsgId -> IO (Maybe (CChatItem 'CTGroup)) +getGroupChatItemByAgentMsgId db user groupId connId msgId = do + itemId_ <- getChatItemIdByAgentMsgId db connId msgId + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupChatItem db user groupId) itemId_ + getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do currentTs <- getCurrentTime @@ -1447,7 +1478,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do -- ChatItem i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.timed_ttl, i.timed_delete_at, i.item_live, -- CIFile - f.file_id, f.file_name, f.file_size, f.file_path, f.ci_file_status, f.protocol, + f.file_id, f.file_name, f.file_size, f.file_path, f.file_crypto_key, f.file_crypto_nonce, f.ci_file_status, f.protocol, -- GroupMember m.group_member_id, m.group_id, m.member_id, m.member_role, m.member_category, m.member_status, m.invited_by, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, @@ -1847,3 +1878,58 @@ deleteCIModeration db GroupInfo {groupId} itemMemberId (Just sharedMsgId) = db "DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ?" (groupId, itemMemberId, sharedMsgId) + +createGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO () +createGroupSndStatus db itemId memberId status = + DB.execute + db + "INSERT INTO group_snd_item_statuses (chat_item_id, group_member_id, group_snd_item_status) VALUES (?,?,?)" + (itemId, memberId, status) + +getGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> ExceptT StoreError IO (CIStatus 'MDSnd) +getGroupSndStatus db itemId memberId = + ExceptT . firstRow fromOnly (SENoGroupSndStatus itemId memberId) $ + DB.query + db + [sql| + SELECT group_snd_item_status + FROM group_snd_item_statuses + WHERE chat_item_id = ? AND group_member_id = ? + LIMIT 1 + |] + (itemId, memberId) + +updateGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO () +updateGroupSndStatus db itemId memberId status = do + currentTs <- liftIO getCurrentTime + DB.execute + db + [sql| + UPDATE group_snd_item_statuses + SET group_snd_item_status = ?, updated_at = ? + WHERE chat_item_id = ? AND group_member_id = ? + |] + (status, currentTs, itemId, memberId) + +getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [(GroupMemberId, CIStatus 'MDSnd)] +getGroupSndStatuses db itemId = + DB.query + db + [sql| + SELECT group_member_id, group_snd_item_status + FROM group_snd_item_statuses + WHERE chat_item_id = ? + |] + (Only itemId) + +getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)] +getGroupSndStatusCounts db itemId = + DB.query + db + [sql| + SELECT group_snd_item_status, COUNT(1) + FROM group_snd_item_statuses + WHERE chat_item_id = ? + GROUP BY group_snd_item_status + |] + (Only itemId) diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index f1fd40d438..d8bab817e3 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -74,6 +74,13 @@ import Simplex.Chat.Migrations.M20230608_deleted_contacts import Simplex.Chat.Migrations.M20230618_favorite_chats import Simplex.Chat.Migrations.M20230621_chat_item_moderations import Simplex.Chat.Migrations.M20230705_delivery_receipts +import Simplex.Chat.Migrations.M20230721_group_snd_item_statuses +import Simplex.Chat.Migrations.M20230814_indexes +import Simplex.Chat.Migrations.M20230827_file_encryption +import Simplex.Chat.Migrations.M20230829_connections_chat_vrange +import Simplex.Chat.Migrations.M20230903_connections_to_subscribe +import Simplex.Chat.Migrations.M20230913_member_contacts +import Simplex.Chat.Migrations.M20230914_member_probes import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -147,7 +154,14 @@ schemaMigrations = ("20230608_deleted_contacts", m20230608_deleted_contacts, Just down_m20230608_deleted_contacts), ("20230618_favorite_chats", m20230618_favorite_chats, Just down_m20230618_favorite_chats), ("20230621_chat_item_moderations", m20230621_chat_item_moderations, Just down_m20230621_chat_item_moderations), - ("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts) + ("20230705_delivery_receipts", m20230705_delivery_receipts, Just down_m20230705_delivery_receipts), + ("20230721_group_snd_item_statuses", m20230721_group_snd_item_statuses, Just down_m20230721_group_snd_item_statuses), + ("20230814_indexes", m20230814_indexes, Just down_m20230814_indexes), + ("20230827_file_encryption", m20230827_file_encryption, Just down_m20230827_file_encryption), + ("20230829_connections_chat_vrange", m20230829_connections_chat_vrange, Just down_m20230829_connections_chat_vrange), + ("20230903_connections_to_subscribe", m20230903_connections_to_subscribe, Just down_m20230903_connections_to_subscribe), + ("20230913_member_contacts", m20230913_member_contacts, Just down_m20230913_member_contacts), + ("20230914_member_probes", m20230914_member_probes, Just down_m20230914_member_probes) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 608befb54d..7f3c9841c0 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -30,6 +30,7 @@ module Simplex.Chat.Store.Profiles updateUserPrivacy, updateAllContactReceipts, updateUserContactReceipts, + updateUserGroupReceipts, updateUserProfile, setUserProfileContactLink, getUserContactProfiles, @@ -65,7 +66,6 @@ import Data.Text (Text) import Data.Text.Encoding (decodeLatin1, encodeUtf8) import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), (:.) (..)) -import qualified Database.SQLite.Simple as DB import Database.SQLite.Simple.QQ (sql) import GHC.Generics (Generic) import Simplex.Chat.Call @@ -77,9 +77,10 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..)) +import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..), SubscriptionMode) import Simplex.Messaging.Transport.Client (TransportHost) import Simplex.Messaging.Util (safeDecodeUtf8) @@ -92,7 +93,7 @@ createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" let showNtfs = True sendRcptsContacts = True - sendRcptsSmallGroups = False + sendRcptsSmallGroups = True DB.execute db "INSERT INTO users (agent_user_id, local_display_name, active_user, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,0,?,?,?,?,?)" @@ -222,13 +223,21 @@ updateUserPrivacy db User {userId, showNtfs, viewPwdHash} = updateAllContactReceipts :: DB.Connection -> Bool -> IO () updateAllContactReceipts db onOff = - DB.execute db "UPDATE users SET send_rcpts_contacts = ? WHERE view_pwd_hash IS NULL" (Only onOff) + DB.execute + db + "UPDATE users SET send_rcpts_contacts = ?, send_rcpts_small_groups = ? WHERE view_pwd_hash IS NULL" + (onOff, onOff) updateUserContactReceipts :: DB.Connection -> User -> UserMsgReceiptSettings -> IO () updateUserContactReceipts db User {userId} UserMsgReceiptSettings {enable, clearOverrides} = do DB.execute db "UPDATE users SET send_rcpts_contacts = ? WHERE user_id = ?" (enable, userId) when clearOverrides $ DB.execute_ db "UPDATE contacts SET send_rcpts = NULL" +updateUserGroupReceipts :: DB.Connection -> User -> UserMsgReceiptSettings -> IO () +updateUserGroupReceipts db User {userId} UserMsgReceiptSettings {enable, clearOverrides} = do + DB.execute db "UPDATE users SET send_rcpts_small_groups = ? WHERE user_id = ?" (enable, userId) + when clearOverrides $ DB.execute_ db "UPDATE groups SET send_rcpts = NULL" + updateUserProfile :: DB.Connection -> User -> Profile -> ExceptT StoreError IO User updateUserProfile db user p' | displayName == newName = do @@ -284,8 +293,8 @@ getUserContactProfiles db User {userId} = toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> (Profile) toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences} -createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> ExceptT StoreError IO () -createUserContactLink db User {userId} agentConnId cReq = +createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO () +createUserContactLink db User {userId} agentConnId cReq subMode = checkConstraint SEDuplicateContactLink . liftIO $ do currentTs <- getCurrentTime DB.execute @@ -293,7 +302,7 @@ createUserContactLink db User {userId} agentConnId cReq = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId Nothing Nothing Nothing 0 currentTs + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode getUserAddressConnections :: DB.Connection -> User -> ExceptT StoreError IO [Connection] getUserAddressConnections db User {userId} = do @@ -307,7 +316,8 @@ getUserAddressConnections db User {userId} = do db [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, - c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter + c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id WHERE c.user_id = ? AND uc.user_id = ? AND uc.local_display_name = '' AND uc.group_id IS NULL @@ -322,6 +332,7 @@ getUserContactLinks db User {userId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.auth_err_counter, + c.peer_chat_min_version, c.peer_chat_max_version, uc.user_contact_link_id, uc.conn_req_contact, uc.group_id FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -388,14 +399,14 @@ data UserContactLink = UserContactLink instance ToJSON UserContactLink where toEncoding = J.genericToEncoding J.defaultOptions data AutoAccept = AutoAccept - { acceptIncognito :: Bool, + { acceptIncognito :: IncognitoEnabled, autoReply :: Maybe MsgContent } deriving (Show, Generic) instance ToJSON AutoAccept where toEncoding = J.genericToEncoding J.defaultOptions -toUserContactLink :: (ConnReqContact, Bool, Bool, Maybe MsgContent) -> UserContactLink +toUserContactLink :: (ConnReqContact, Bool, IncognitoEnabled, Maybe MsgContent) -> UserContactLink toUserContactLink (connReq, autoAccept, acceptIncognito, autoReply) = UserContactLink connReq $ if autoAccept then Just AutoAccept {acceptIncognito, autoReply} else Nothing @@ -443,9 +454,6 @@ updateUserAddressAutoAccept db user@User {userId} autoAccept = do Just AutoAccept {acceptIncognito, autoReply} -> (True, acceptIncognito, autoReply) _ -> (False, False, Nothing) - - - getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p] getProtocolServers db User {userId} = map toServerCfg diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 6f368a774a..0e146bb992 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -17,15 +17,15 @@ import Control.Monad.Except import Crypto.Random (ChaChaDRG, randomBytesGenerate) import Data.Aeson (ToJSON) import qualified Data.Aeson as J -import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Base64 as B64 +import Data.ByteString.Char8 (ByteString) import Data.Int (Int64) import Data.Maybe (fromMaybe, isJust, listToMaybe) import Data.Text (Text) import qualified Data.Text as T import Data.Time.Clock (UTCTime (..), getCurrentTime) import Database.SQLite.Simple (NamedParam (..), Only (..), Query, SQLError, (:.) (..)) -import qualified Database.SQLite.Simple as DB +import qualified Database.SQLite.Simple as SQL import Database.SQLite.Simple.QQ (sql) import GHC.Generics (Generic) import Simplex.Chat.Messages @@ -34,8 +34,11 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Util (allFinally) +import Simplex.Messaging.Version import UnliftIO.STM -- These error type constructors must be added to mobile apps @@ -49,6 +52,7 @@ data StoreError | SEUserNotFoundByContactRequestId {contactRequestId :: Int64} | SEContactNotFound {contactId :: ContactId} | SEContactNotFoundByName {contactName :: ContactName} + | SEContactNotFoundByMemberId {groupMemberId :: GroupMemberId} | SEContactNotReady {contactName :: ContactName} | SEDuplicateContactLink | SEUserContactLinkNotFound @@ -59,6 +63,7 @@ data StoreError | SEGroupMemberNameNotFound {groupId :: GroupId, groupMemberName :: ContactName} | SEGroupMemberNotFound {groupMemberId :: GroupMemberId} | SEGroupMemberNotFoundByMemberId {memberId :: MemberId} + | SEMemberContactGroupMemberNotFound {contactId :: ContactId} | SEGroupWithoutUser | SEDuplicateGroupMember | SEGroupAlreadyJoined @@ -76,6 +81,7 @@ data StoreError | SERcvFileNotFoundXFTP {agentRcvFileId :: AgentRcvFileId} | SEConnectionNotFound {agentConnId :: AgentConnId} | SEConnectionNotFoundById {connId :: Int64} + | SEConnectionNotFoundByMemberId {groupMemberId :: GroupMemberId} | SEPendingConnectionNotFound {connId :: Int64} | SEIntroNotFound | SEUniqueID @@ -92,6 +98,7 @@ data StoreError | SEGroupLinkNotFound {groupInfo :: GroupInfo} | SEHostMemberIdNotFound {groupId :: Int64} | SEContactNotFoundByFileId {fileId :: FileTransferId} + | SENoGroupSndStatus {itemId :: ChatItemId, groupMemberId :: GroupMemberId} deriving (Show, Exception, Generic) instance ToJSON StoreError where @@ -106,7 +113,7 @@ checkConstraint err action = ExceptT $ runExceptT action `E.catch` (pure . Left handleSQLError :: StoreError -> SQLError -> StoreError handleSQLError err e - | DB.sqlError e == DB.ErrorConstraint = err + | SQL.sqlError e == SQL.ErrorConstraint = err | otherwise = SEInternalError $ show e storeFinally :: ExceptT StoreError IO a -> ExceptT StoreError IO b -> ExceptT StoreError IO a @@ -130,15 +137,16 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, Int, Version, Version) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe Int, Maybe Version, Maybe Version) toConnection :: ConnectionRow -> Connection -toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) = +toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) = let entityId = entityId_ connType connectionCode = SecurityCode <$> code_ <*> verifiedAt_ - in Connection {connId, agentConnId = AgentConnId acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt} + peerChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer + in Connection {connId, agentConnId = AgentConnId acId, peerChatVRange, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias, entityId, connectionCode, authErrCounter, createdAt} where entityId_ :: ConnType -> Maybe Int64 entityId_ ConnContact = contactId @@ -148,12 +156,12 @@ toConnection ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroup entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: MaybeConnectionRow -> Maybe Connection -toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter)) = - Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter)) +toMaybeConnection ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just authErrCounter, Just minVer, Just maxVer)) = + Just $ toConnection ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, authErrCounter, minVer, maxVer)) toMaybeConnection _ = Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> IO Connection -createConnection_ db userId connType entityId acId viaContact viaUserContactLink customUserProfileId connLevel currentTs = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionRange -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> IO Connection +createConnection_ db userId connType entityId acId peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode = do viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId -> maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId) let viaGroupLink = isJust viaLinkGroupId @@ -162,17 +170,30 @@ createConnection_ db userId connType entityId acId viaContact viaUserContactLink [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, custom_user_profile_id, conn_status, conn_type, - contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, created_at, updated_at, + peer_chat_min_version, peer_chat_max_version, to_subscribe + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) + :. (minV, maxV, subMode == SMOnlyCreate) ) connId <- insertedRowId db - pure Connection {connId, agentConnId = AgentConnId acId, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} + pure Connection {connId, agentConnId = AgentConnId acId, peerChatVRange = JVersionRange peerChatVRange, connType, entityId, viaContact, viaUserContactLink, viaGroupLink, groupLinkId = Nothing, customUserProfileId, connLevel, connStatus = ConnNew, localAlias = "", createdAt = currentTs, connectionCode = Nothing, authErrCounter = 0} where ent ct = if connType == ct then entityId else Nothing +setPeerChatVRange :: DB.Connection -> Int64 -> VersionRange -> IO () +setPeerChatVRange db connId (VersionRange minVer maxVer) = + DB.execute + db + [sql| + UPDATE connections + SET peer_chat_min_version = ?, peer_chat_max_version = ? + WHERE connection_id = ? + |] + (minVer, maxVer, connId) + setCommandConnId :: DB.Connection -> User -> CommandId -> Int64 -> IO () setCommandConnId db User {userId} cmdId connId = do updatedAt <- getCurrentTime @@ -202,7 +223,7 @@ createContact_ db userId connId Profile {displayName, fullName, image, contactLi pure $ Right (ldn, contactId, profileId) deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () -deleteUnusedIncognitoProfileById_ db User {userId} profile_id = +deleteUnusedIncognitoProfileById_ db User {userId} profileId = DB.executeNamed db [sql| @@ -217,26 +238,26 @@ deleteUnusedIncognitoProfileById_ db User {userId} profile_id = WHERE user_id = :user_id AND member_profile_id = :profile_id LIMIT 1 ) |] - [":user_id" := userId, ":profile_id" := profile_id] + [":user_id" := userId, ":profile_id" := profileId] -type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) +type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool) :. (Maybe Bool, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool) toContact :: User -> ContactRow :. ConnectionRow -> Contact -toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = +toContact user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} activeConn = toConnection connRow chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} toContactOrError :: User -> ContactRow :. MaybeConnectionRow -> Either StoreError Contact -toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs)) :. connRow) = +toContactOrError user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe True enableNtfs_, sendRcpts, favorite} in case toMaybeConnection connRow of Just activeConn -> let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito activeConn - in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs} + in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} _ -> Left $ SEContactNotReady localDisplayName getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile @@ -254,12 +275,13 @@ getProfileById db userId profileId = toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} -type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime) +type ContactRequestRow = (Int64, ContactName, AgentInvId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, Maybe Preferences, UTCTime, UTCTime, Version, Version) toContactRequest :: ContactRequestRow -> UserContactRequest -toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt)) = do +toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, preferences, createdAt, updatedAt, minVer, maxVer)) = do let profile = Profile {displayName, fullName, image, contactLink, preferences} - in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt} + cReqChatVRange = JVersionRange $ fromMaybe (versionToRange maxVer) $ safeVersionRange minVer maxVer + in UserContactRequest {contactRequestId, agentInvitationId, userContactLinkId, agentContactConnId, cReqChatVRange, localDisplayName, profileId, profile, xContactId, createdAt, updatedAt} userQuery :: Query userQuery = @@ -283,6 +305,14 @@ toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Mayb toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) = PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt} +getConnReqInv :: DB.Connection -> Int64 -> ExceptT StoreError IO ConnReqInvitation +getConnReqInv db connId = + ExceptT . firstRow fromOnly (SEConnectionNotFoundById connId) $ + DB.query + db + "SELECT conn_req_inv FROM connections WHERE connection_id = ?" + (Only connId) + -- | Saves unique local display name based on passed displayName, suffixed with _N if required. -- This function should be called inside transaction. withLocalDisplayName :: forall a. DB.Connection -> UserId -> Text -> (Text -> IO (Either StoreError a)) -> IO (Either StoreError a) @@ -308,7 +338,7 @@ withLocalDisplayName db userId displayName action = getLdnSuffix >>= (`tryCreate E.try (insertName ldn currentTs) >>= \case Right () -> action ldn Left e - | DB.sqlError e == DB.ErrorConstraint -> tryCreateName (ldnSuffix + 1) (attempts - 1) + | SQL.sqlError e == SQL.ErrorConstraint -> tryCreateName (ldnSuffix + 1) (attempts - 1) | otherwise -> E.throwIO e where insertName ldn ts = @@ -334,7 +364,7 @@ createWithRandomBytes size gVar create = tryCreate 3 liftIO (E.try $ create id') >>= \case Right x -> pure x Left e - | DB.sqlError e == DB.ErrorConstraint -> tryCreate (n - 1) + | SQL.sqlError e == SQL.ErrorConstraint -> tryCreate (n - 1) | otherwise -> throwError . SEInternalError $ show e encodedRandomBytes :: TVar ChaChaDRG -> Int -> IO ByteString diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index ed6c3203d2..36cec49d7c 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -25,7 +25,7 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Database.SQLite.Simple (Only (..)) -import qualified Database.SQLite.Simple as DB +import qualified Database.SQLite.Simple as SQL import Database.SQLite.Simple.QQ (sql) import GHC.Weak (deRefWeak) import Simplex.Chat @@ -36,6 +36,7 @@ import Simplex.Chat.Styled import Simplex.Chat.Terminal.Output import Simplex.Chat.Types (User (..)) import Simplex.Messaging.Agent.Store.SQLite (SQLiteStore, withTransaction) +import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import Simplex.Messaging.Util (catchAll_, safeDecodeUtf8, whenM) import System.Exit (exitSuccess) import System.Terminal hiding (insertChars) @@ -299,7 +300,7 @@ updateTermState user_ st ac live tw (key, ms) ts@TerminalState {inputString = s, getNameSfxs table pfx = getNameSfxs_ pfx (userId, pfx <> "%") $ "SELECT local_display_name FROM " <> table <> " WHERE user_id = ? AND local_display_name LIKE ?" - getNameSfxs_ :: DB.ToRow p => Text -> p -> DB.Query -> IO [String] + getNameSfxs_ :: SQL.ToRow p => Text -> p -> SQL.Query -> IO [String] getNameSfxs_ pfx ps q = withTransaction st (\db -> hasPfx pfx . map fromOnly <$> DB.query db q ps) `catchAll_` pure [] commands = diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 33a6bd0456..dea948e842 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -23,7 +23,7 @@ module Simplex.Chat.Types where import Crypto.Number.Serialize (os2ip) -import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson (FromJSON (..), ToJSON (..), (.=)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.Types as JT @@ -42,10 +42,12 @@ import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, fromTextField_, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) import Simplex.Messaging.Util ((<$?>)) +import Simplex.Messaging.Version class IsContact a where contactId' :: a -> ContactId @@ -170,7 +172,9 @@ data Contact = Contact mergedPreferences :: ContactUserPreferences, createdAt :: UTCTime, updatedAt :: UTCTime, - chatTs :: Maybe UTCTime + chatTs :: Maybe UTCTime, + contactGroupMemberId :: Maybe GroupMemberId, + contactGrpInvSent :: Bool } deriving (Eq, Show, Generic) @@ -184,7 +188,9 @@ contactConn = activeConn contactConnId :: Contact -> ConnId contactConnId = aConnId . contactConn -contactConnIncognito :: Contact -> Bool +type IncognitoEnabled = Bool + +contactConnIncognito :: Contact -> IncognitoEnabled contactConnIncognito = connIncognito . contactConn contactDirect :: Contact -> Bool @@ -210,6 +216,19 @@ data ContactRef = ContactRef instance ToJSON ContactRef where toEncoding = J.genericToEncoding J.defaultOptions +data ContactOrGroupMember = CGMContact Contact | CGMGroupMember GroupInfo GroupMember + deriving (Show) + +contactOrGroupMemberIds :: ContactOrGroupMember -> (Maybe ContactId, Maybe GroupMemberId) +contactOrGroupMemberIds = \case + CGMContact Contact {contactId} -> (Just contactId, Nothing) + CGMGroupMember _ GroupMember {groupMemberId} -> (Nothing, Just groupMemberId) + +contactOrGroupMemberIncognito :: ContactOrGroupMember -> IncognitoEnabled +contactOrGroupMemberIncognito = \case + CGMContact ct -> contactConnIncognito ct + CGMGroupMember _ m -> memberIncognito m + data UserContact = UserContact { userContactLinkId :: Int64, connReqContact :: ConnReqContact, @@ -229,6 +248,7 @@ data UserContactRequest = UserContactRequest agentInvitationId :: AgentInvId, userContactLinkId :: Int64, agentContactConnId :: AgentConnId, -- connection id of user contact + cReqChatVRange :: JVersionRange, localDisplayName :: ContactName, profileId :: Int64, profile :: Profile, @@ -318,6 +338,13 @@ instance ToJSON GroupInfo where toEncoding = J.genericToEncoding J.defaultOption groupName' :: GroupInfo -> GroupName groupName' GroupInfo {localDisplayName = g} = g +data GroupSummary = GroupSummary + { currentMembers :: Int + } + deriving (Show, Generic) + +instance ToJSON GroupSummary where toEncoding = J.genericToEncoding J.defaultOptions + data ContactOrGroup = CGContact Contact | CGGroup Group contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId) @@ -336,11 +363,12 @@ data ChatSettings = ChatSettings instance ToJSON ChatSettings where toEncoding = J.genericToEncoding J.defaultOptions defaultChatSettings :: ChatSettings -defaultChatSettings = ChatSettings - { enableNtfs = True, - sendRcpts = Nothing, - favorite = False - } +defaultChatSettings = + ChatSettings + { enableNtfs = True, + sendRcpts = Nothing, + favorite = False + } pattern DisableNtfs :: ChatSettings pattern DisableNtfs <- ChatSettings {enableNtfs = False} @@ -412,10 +440,10 @@ instance ToJSON Profile where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} -- check if profiles match ignoring preferences -profilesMatch :: Profile -> Profile -> Bool +profilesMatch :: LocalProfile -> LocalProfile -> Bool profilesMatch - Profile {displayName = n1, fullName = fn1, image = i1} - Profile {displayName = n2, fullName = fn2, image = i2} = + LocalProfile {displayName = n1, fullName = fn1, image = i1} + LocalProfile {displayName = n2, fullName = fn2, image = i2} = n1 == n2 && fn1 == fn2 && i1 == i2 data IncognitoProfile = NewIncognito Profile | ExistingIncognito LocalProfile @@ -527,24 +555,31 @@ instance ToJSON MemberIdRole where toEncoding = J.genericToEncoding J.defaultOpt data IntroInvitation = IntroInvitation { groupConnReq :: ConnReqInvitation, - directConnReq :: ConnReqInvitation + directConnReq :: Maybe ConnReqInvitation } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON IntroInvitation where toEncoding = J.genericToEncoding J.defaultOptions +instance ToJSON IntroInvitation where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} data MemberInfo = MemberInfo { memberId :: MemberId, memberRole :: GroupMemberRole, + v :: Maybe ChatVersionRange, profile :: Profile } deriving (Eq, Show, Generic, FromJSON) -instance ToJSON MemberInfo where toEncoding = J.genericToEncoding J.defaultOptions +instance ToJSON MemberInfo where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} memberInfo :: GroupMember -> MemberInfo -memberInfo GroupMember {memberId, memberRole, memberProfile} = - MemberInfo memberId memberRole (fromLocalProfile memberProfile) +memberInfo GroupMember {memberId, memberRole, memberProfile, activeConn} = + MemberInfo memberId memberRole memberChatVRange (fromLocalProfile memberProfile) + where + memberChatVRange = ChatVersionRange . fromJVersionRange . peerChatVRange <$> activeConn data ReceivedGroupInvitation = ReceivedGroupInvitation { fromMember :: GroupMember, @@ -566,8 +601,13 @@ data GroupMember = GroupMember memberStatus :: GroupMemberStatus, invitedBy :: InvitedBy, localDisplayName :: ContactName, + -- for membership, memberProfile can be either user's profile or incognito profile, based on memberIncognito test. + -- for other members it's whatever profile the local user can see (there is no info about whether it's main or incognito profile for remote users). memberProfile :: LocalProfile, + -- this is the ID of the associated contact (it will be used to send direct messages to the member) memberContactId :: Maybe ContactId, + -- for membership it would always point to user's contact + -- it is used to test for incognito status by comparing with ID in memberProfile memberContactProfileId :: ProfileId, activeConn :: Maybe Connection } @@ -595,9 +635,18 @@ memberConnId GroupMember {activeConn} = aConnId <$> activeConn groupMemberId' :: GroupMember -> GroupMemberId groupMemberId' GroupMember {groupMemberId} = groupMemberId -memberIncognito :: GroupMember -> Bool +memberIncognito :: GroupMember -> IncognitoEnabled memberIncognito GroupMember {memberProfile, memberContactProfileId} = localProfileId memberProfile /= memberContactProfileId +incognitoMembership :: GroupInfo -> IncognitoEnabled +incognitoMembership GroupInfo {membership} = memberIncognito membership + +-- returns profile when membership is incognito, otherwise Nothing +incognitoMembershipProfile :: GroupInfo -> Maybe LocalProfile +incognitoMembershipProfile GroupInfo {membership = m@GroupMember {memberProfile}} + | memberIncognito m = Just memberProfile + | otherwise = Nothing + memberSecurityCode :: GroupMember -> Maybe SecurityCode memberSecurityCode GroupMember {activeConn} = connectionCode =<< activeConn @@ -782,7 +831,11 @@ memberActive m = case memberStatus m of GSMemCreator -> True memberCurrent :: GroupMember -> Bool -memberCurrent m = case memberStatus m of +memberCurrent = memberCurrent' . memberStatus + +-- update getGroupSummary if this is changed +memberCurrent' :: GroupMemberStatus -> Bool +memberCurrent' = \case GSMemRemoved -> False GSMemLeft -> False GSMemGroupDeleted -> False @@ -931,7 +984,10 @@ data RcvFileTransfer = RcvFileTransfer senderDisplayName :: ContactName, chunkSize :: Integer, cancelled :: Bool, - grpMemberId :: Maybe Int64 + grpMemberId :: Maybe Int64, + -- XFTP files are encrypted as they are received, they are never stored unecrypted + -- SMP files are encrypted after all chunks are received + cryptoArgs :: Maybe CryptoFileArgs } deriving (Eq, Show, Generic) @@ -1095,7 +1151,8 @@ instance ToJSON FileTransferMeta where toEncoding = J.genericToEncoding J.defaul data XFTPSndFile = XFTPSndFile { agentSndFileId :: AgentSndFileId, privateSndFileDescr :: Maybe Text, - agentSndFileDeleted :: Bool + agentSndFileDeleted :: Bool, + cryptoArgs :: Maybe CryptoFileArgs } deriving (Eq, Show, Generic) @@ -1141,6 +1198,7 @@ type ConnReqContact = ConnectionRequestUri 'CMContact data Connection = Connection { connId :: Int64, agentConnId :: AgentConnId, + peerChatVRange :: JVersionRange, connLevel :: Int, viaContact :: Maybe Int64, -- group member contact ID, if not direct connection viaUserContactLink :: Maybe Int64, -- user contact link ID, if connected via "user address" @@ -1333,8 +1391,6 @@ serializeIntroStatus = \case data Notification = Notification {title :: Text, text :: Text} -type JSONString = String - textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode @@ -1451,3 +1507,21 @@ instance ProtocolTypeI p => ToJSON (ServerCfg p) where instance ProtocolTypeI p => FromJSON (ServerCfg p) where parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True} + +newtype ChatVersionRange = ChatVersionRange {fromChatVRange :: VersionRange} deriving (Eq, Show) + +chatInitialVRange :: VersionRange +chatInitialVRange = versionToRange 1 + +instance FromJSON ChatVersionRange where + parseJSON v = ChatVersionRange <$> strParseJSON "ChatVersionRange" v + +instance ToJSON ChatVersionRange where + toJSON (ChatVersionRange vr) = strToJSON vr + toEncoding (ChatVersionRange vr) = strToJEncoding vr + +newtype JVersionRange = JVersionRange {fromJVersionRange :: VersionRange} deriving (Eq, Show) + +instance ToJSON JVersionRange where + toJSON (JVersionRange (VersionRange minV maxV)) = J.object ["minVersion" .= minV, "maxVersion" .= maxV] + toEncoding (JVersionRange (VersionRange minV maxV)) = J.pairs $ "minVersion" .= minV <> "maxVersion" .= maxV diff --git a/src/Simplex/Chat/Util.hs b/src/Simplex/Chat/Util.hs index 7a350705f1..46b5be28b3 100644 --- a/src/Simplex/Chat/Util.hs +++ b/src/Simplex/Chat/Util.hs @@ -1,6 +1,32 @@ -module Simplex.Chat.Util (week) where +module Simplex.Chat.Util (week, encryptFile, chunkSize) where +import Control.Monad +import Control.Monad.Except +import Control.Monad.IO.Class +import qualified Data.ByteString.Lazy as LB import Data.Time (NominalDiffTime) +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF +import UnliftIO.IO (IOMode (..), withFile) week :: NominalDiffTime week = 7 * 86400 + +encryptFile :: FilePath -> FilePath -> CryptoFileArgs -> ExceptT String IO () +encryptFile fromPath toPath cfArgs = do + let toFile = CryptoFile toPath $ Just cfArgs + -- uncomment to test encryption error in runTestFileTransferEncrypted + -- throwError "test error" + withExceptT show $ + withFile fromPath ReadMode $ \r -> CF.withFile toFile WriteMode $ \w -> do + encryptChunks r w + liftIO $ CF.hPutTag w + where + encryptChunks r w = do + ch <- liftIO $ LB.hGet r chunkSize + unless (LB.null ch) $ liftIO $ CF.hPut w ch + unless (LB.length ch < chunkSize) $ encryptChunks r w + +chunkSize :: Num a => a +chunkSize = 65536 +{-# INLINE chunkSize #-} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index d079e1fccb..e985156290 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -20,6 +20,8 @@ import Data.Int (Int64) import Data.List (groupBy, intercalate, intersperse, partition, sortOn) import Data.List.NonEmpty (NonEmpty) import qualified Data.List.NonEmpty as L +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe) import Data.Text (Text) import qualified Data.Text as T @@ -44,10 +46,12 @@ import Simplex.Chat.Styled import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import qualified Simplex.FileTransfer.Protocol as XFTP -import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..)) +import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) import Simplex.Messaging.Agent.Protocol +import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) @@ -55,6 +59,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, Pro import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Util (bshow, tshow) +import Simplex.Messaging.Version hiding (version) import System.Console.ANSI.Types type CurrentTime = UTCTime @@ -79,6 +84,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRNetworkConfig cfg -> viewNetworkConfig cfg CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile + CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats CRContactSwitchStarted {} -> ["switch started"] CRGroupMemberSwitchStarted {} -> ["switch started"] @@ -115,6 +121,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView HSGroups -> groupsHelpInfo HSContacts -> contactsHelpInfo HSMyAddress -> myAddressHelpInfo + HSIncognito -> incognitoHelpInfo HSMessages -> messagesHelpInfo HSMarkdown -> markdownInfo HSSettings -> settingsInfo @@ -138,7 +145,8 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u' CRVersionInfo info _ _ -> viewVersionInfo logLevel info - CRInvitation u cReq -> ttyUser u $ viewConnReqInvitation cReq + CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq + CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c CRSentConfirmation u -> ttyUser u ["confirmation sent!"] CRSentInvitation u customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] @@ -155,11 +163,11 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRRcvFileDescrReady _ _ -> [] CRRcvFileDescrNotReady _ _ -> [] CRRcvFileProgressXFTP {} -> [] - CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci + CRRcvFileAccepted u ci -> ttyUser u $ savingFile' testView ci CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRSndFileCancelled u _ ftm fts -> ttyUser u $ viewSndFileCancelled ftm fts CRRcvFileCancelled u _ ft -> ttyUser u $ receivingFile_ "cancelled" ft - CRUserProfileUpdated u p p' s f -> ttyUser u $ viewUserProfileUpdated p p' s f + CRUserProfileUpdated u p p' summary -> ttyUser u $ viewUserProfileUpdated p p' summary CRUserProfileImage u p -> ttyUser u $ viewUserProfileImage p CRContactPrefsUpdated {user = u, fromContact, toContact} -> ttyUser u $ viewUserContactPrefsUpdated u fromContact toContact CRContactAliasUpdated u c -> ttyUser u $ viewContactAliasUpdated c @@ -170,7 +178,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRRcvFileStart u ci -> ttyUser u $ receivingFile_' "started" ci CRRcvFileComplete u ci -> ttyUser u $ receivingFile_' "completed" ci CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft - CRRcvFileError u ci -> ttyUser u $ receivingFile_' "error" ci + CRRcvFileError u ci e -> ttyUser u $ receivingFile_' "error" ci <> [sShow e] CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndFileStartXFTP {} -> [] @@ -200,7 +208,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView addressSS UserContactSubStatus {userContactError} = maybe ("Your address is active! To show: " <> highlight' "/sa") (\e -> "User address error: " <> sShow e <> ", to delete your address: " <> highlight' "/da") userContactError (groupLinkErrors, groupLinksSubscribed) = partition (isJust . userContactError) groupLinks CRGroupInvitation u g -> ttyUser u [groupInvitation' g] - CRReceivedGroupInvitation u g c role -> ttyUser u $ viewReceivedGroupInvitation g c role + CRReceivedGroupInvitation {user = u, groupInfo = g, contact = c, memberRole = r} -> ttyUser u $ viewReceivedGroupInvitation g c r CRUserJoinedGroup u g _ -> ttyUser u $ viewUserJoinedGroup g CRJoinedGroupMember u g m -> ttyUser u $ viewJoinedGroupMember g m CRHostConnected p h -> [plain $ "connected to " <> viewHostEvent p h] @@ -217,10 +225,16 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRGroupDeleted u g m -> ttyUser u [ttyGroup' g <> ": " <> ttyMember m <> " deleted the group", "use " <> highlight ("/d #" <> groupName' g) <> " to delete the local copy of the group"] CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m CRGroupProfile u g -> ttyUser u $ viewGroupProfile g + CRGroupDescription u g -> ttyUser u $ viewGroupDescription g CRGroupLinkCreated u g cReq mRole -> ttyUser u $ groupLink_ "Group link is created!" g cReq mRole CRGroupLink u g cReq mRole -> ttyUser u $ groupLink_ "Group link:" g cReq mRole CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g CRAcceptingGroupJoinRequest _ g c -> [ttyFullContact c <> ": accepting request to join group " <> ttyGroup' g <> "..."] + CRNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] + CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"] + CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m] + CRNewMemberContactReceivedInv u ct g m -> ttyUser u [ttyGroup' g <> " " <> ttyMember m <> " is creating direct contact " <> ttyContact' ct <> " with you"] + CRMemberContactConnected u ct g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " is merged into " <> ttyContact' ct] CRMemberSubError u g m e -> ttyUser u [ttyGroup' g <> " member " <> ttyMember m <> " error: " <> sShow e] CRMemberSubSummary u summary -> ttyUser u $ viewErrorsSummary (filter (isJust . memberError) summary) " group member errors" CRGroupSubscribed u g -> ttyUser u $ viewGroupSubscribed g @@ -243,14 +257,33 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView CRNtfToken _ status mode -> ["device token status: " <> plain (smpEncode status) <> ", notifications mode: " <> plain (strEncode mode)] CRNtfMessages {} -> [] CRSQLResult rows -> map plain rows + CRSlowSQLQueries {chatQueries, agentQueries} -> + let viewQuery SlowSQLQuery {query, queryStats = SlowQueryStats {count, timeMax, timeAvg}} = + ("count: " <> sShow count) + <> (" :: max: " <> sShow timeMax <> " ms") + <> (" :: avg: " <> sShow timeAvg <> " ms") + <> (" :: " <> plain (T.unwords $ T.lines query)) + in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) CRDebugLocks {chatLockName, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) ] CRAgentStats stats -> map (plain . intercalate ",") stats + CRAgentSubs {activeSubs, pendingSubs, removedSubs} -> + [plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)] + <> ("active subscriptions:" : listSubs activeSubs) + <> ("pending subscriptions:" : listSubs pendingSubs) + <> ("removed subscriptions:" : listSubs removedSubs) + where + listSubs :: Show a => Map Text a -> [StyledString] + listSubs = map (\(srv, info) -> plain $ srv <> ": " <> tshow info) . M.assocs + CRAgentSubsDetails SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} -> + ("active subscriptions:" : map sShow activeSubscriptions) + <> ("pending subscriptions: " : map sShow pendingSubscriptions) + <> ("removed subscriptions: " : map sShow removedSubscriptions) CRConnectionDisabled entity -> viewConnectionEntityDisabled entity CRAgentRcvQueueDeleted acId srv aqId err_ -> - [ "completed deleting rcv queue, agent connection id: " <> sShow acId + [ ("completed deleting rcv queue, agent connection id: " <> sShow acId) <> (", server: " <> sShow srv) <> (", agent queue id: " <> sShow aqId) <> maybe "" (\e -> ", error: " <> sShow e) err_ @@ -303,7 +336,7 @@ responseToView user_ ChatConfig {logLevel, showReactions, showReceipts, testView Just CIQuote {chatDir = quoteDir, content} -> Just (msgDirectionInt $ quoteMsgDirection quoteDir, msgContentText content) fPath = case file of - Just CIFile {filePath = Just fp} -> Just fp + Just CIFile {fileSource = Just (CryptoFile fp _)} -> Just fp _ -> Nothing testViewItem :: CChatItem c -> Maybe GroupMember -> Text testViewItem (CChatItem _ ci@ChatItem {meta = CIMeta {itemText}}) membership_ = @@ -465,12 +498,21 @@ localTs tz ts = do viewChatItemStatusUpdated :: AChatItem -> CurrentTime -> TimeZone -> Bool -> Bool -> [StyledString] viewChatItemStatusUpdated (AChatItem _ _ chat item@ChatItem {meta = CIMeta {itemStatus}}) ts tz testView showReceipts = case itemStatus of - CISSndRcvd rcptStatus -> + CISSndRcvd rcptStatus SSPPartial -> + if testView && showReceipts + then prependFirst (viewDeliveryReceiptPartial rcptStatus <> " ") $ viewChatItem chat item False ts tz + else [] + CISSndRcvd rcptStatus SSPComplete -> if testView && showReceipts then prependFirst (viewDeliveryReceipt rcptStatus <> " ") $ viewChatItem chat item False ts tz else [] _ -> [] +viewDeliveryReceiptPartial :: MsgReceiptStatus -> StyledString +viewDeliveryReceiptPartial = \case + MROk -> "%" + MRBadMsgHash -> ttyError' "%!" + viewDeliveryReceipt :: MsgReceiptStatus -> StyledString viewDeliveryReceipt = \case MROk -> "⩗" @@ -625,6 +667,17 @@ viewConnReqInvitation cReq = "and ask them to connect: " <> highlight' "/c " ] +viewContactNotFound :: ContactName -> Maybe (GroupInfo, GroupMember) -> [StyledString] +viewContactNotFound cName suspectedMember = + ["no contact " <> ttyContact cName <> useMessageMember] + where + useMessageMember = case suspectedMember of + Just (g, m) -> do + let GroupInfo {localDisplayName = gName} = g + GroupMember {localDisplayName = mName} = m + ", use " <> highlight' ("@#" <> T.unpack gName <> " " <> T.unpack mName <> " ") + _ -> "" + viewChatCleared :: AChatInfo -> [StyledString] viewChatCleared (AChatInfo _ chatInfo) = case chatInfo of DirectChat ct -> [ttyContact' ct <> ": all messages are removed locally ONLY"] @@ -720,21 +773,21 @@ viewDirectMessagesProhibited MDSnd c = ["direct messages to indirect contact " < viewDirectMessagesProhibited MDRcv c = ["received prohibited direct message from indirect contact " <> ttyContact' c <> " (discarded)"] viewUserJoinedGroup :: GroupInfo -> [StyledString] -viewUserJoinedGroup g@GroupInfo {membership = membership@GroupMember {memberProfile}} = - if memberIncognito membership - then [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)] - else [ttyGroup' g <> ": you joined the group"] +viewUserJoinedGroup g = + case incognitoMembershipProfile g of + Just mp -> [ttyGroup' g <> ": you joined the group incognito as " <> incognitoProfile' (fromLocalProfile mp)] + Nothing -> [ttyGroup' g <> ": you joined the group"] viewJoinedGroupMember :: GroupInfo -> GroupMember -> [StyledString] viewJoinedGroupMember g m = [ttyGroup' g <> ": " <> ttyMember m <> " joined the group "] viewReceivedGroupInvitation :: GroupInfo -> Contact -> GroupMemberRole -> [StyledString] -viewReceivedGroupInvitation g@GroupInfo {membership = membership@GroupMember {memberProfile}} c role = +viewReceivedGroupInvitation g c role = ttyFullGroup g <> ": " <> ttyContact' c <> " invites you to join the group as " <> plain (strEncode role) : - if memberIncognito membership - then ["use " <> highlight ("/j " <> groupName' g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile memberProfile)] - else ["use " <> highlight ("/j " <> groupName' g) <> " to accept"] + case incognitoMembershipProfile g of + Just mp -> ["use " <> highlight ("/j " <> groupName' g) <> " to join incognito as " <> incognitoProfile' (fromLocalProfile mp)] + Nothing -> ["use " <> highlight ("/j " <> groupName' g) <> " to accept"] groupPreserved :: GroupInfo -> [StyledString] groupPreserved g = ["use " <> highlight ("/d #" <> groupName' g) <> " to delete the group"] @@ -801,12 +854,12 @@ viewContactConnected ct@Contact {localDisplayName} userIncognitoProfile testView Nothing -> [ttyFullContact ct <> ": contact is connected"] -viewGroupsList :: [GroupInfo] -> [StyledString] +viewGroupsList :: [(GroupInfo, GroupSummary)] -> [StyledString] viewGroupsList [] = ["you have no groups!", "to create: " <> highlight' "/g "] viewGroupsList gs = map groupSS $ sortOn ldn_ gs where - ldn_ = T.toLower . (localDisplayName :: GroupInfo -> GroupName) - groupSS g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership, chatSettings} = + ldn_ = T.toLower . (localDisplayName :: GroupInfo -> GroupName) . fst + groupSS (g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership, chatSettings}, GroupSummary {currentMembers}) = case memberStatus membership of GSMemInvited -> groupInvitation' g s -> membershipIncognito g <> ttyGroup ldn <> optFullName ldn fullName <> viewMemberStatus s @@ -816,12 +869,13 @@ viewGroupsList gs = map groupSS $ sortOn ldn_ gs GSMemLeft -> delete "you left" GSMemGroupDeleted -> delete "group deleted" _ - | enableNtfs chatSettings -> "" - | otherwise -> " (muted, you can " <> highlight ("/unmute #" <> ldn) <> ")" + | enableNtfs chatSettings -> " (" <> memberCount <> ")" + | otherwise -> " (" <> memberCount <> ", muted, you can " <> highlight ("/unmute #" <> ldn) <> ")" delete reason = " (" <> reason <> ", delete local copy: " <> highlight ("/d #" <> ldn) <> ")" + memberCount = sShow currentMembers <> " member" <> if currentMembers == 1 then "" else "s" groupInvitation' :: GroupInfo -> StyledString -groupInvitation' GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}, membership = membership@GroupMember {memberProfile}} = +groupInvitation' g@GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} = highlight ("#" <> ldn) <> optFullName ldn fullName <> " - you are invited (" @@ -830,10 +884,9 @@ groupInvitation' GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile <> highlight ("/d #" <> ldn) <> " to delete invitation)" where - joinText = - if memberIncognito membership - then " to join as " <> incognitoProfile' (fromLocalProfile memberProfile) <> ", " - else " to join, " + joinText = case incognitoMembershipProfile g of + Just mp -> " to join as " <> incognitoProfile' (fromLocalProfile mp) <> ", " + Nothing -> " to join, " viewContactsMerged :: Contact -> Contact -> [StyledString] viewContactsMerged _into@Contact {localDisplayName = c1} _merged@Contact {localDisplayName = c2} = @@ -915,8 +968,9 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = ] viewContactInfo :: Contact -> ConnectionStats -> Maybe Profile -> [StyledString] -viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}} stats incognitoProfile = - ["contact ID: " <> sShow contactId] <> viewConnectionStats stats +viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile = + ["contact ID: " <> sShow contactId] + <> viewConnectionStats stats <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) l]) contactLink <> maybe ["you've shared main profile with this contact"] @@ -924,20 +978,31 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta incognitoProfile <> ["alias: " <> plain localAlias | localAlias /= ""] <> [viewConnectionVerified (contactSecurityCode ct)] + <> [viewPeerChatVRange (peerChatVRange activeConn)] + +viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString] +viewGroupInfo GroupInfo {groupId} s = + [ "group ID: " <> sShow groupId, + "current members: " <> sShow (currentMembers s) + ] viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] -viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}} stats = +viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias}, activeConn} stats = [ "group ID: " <> sShow groupId, "member ID: " <> sShow groupMemberId ] <> maybe ["member not connected"] viewConnectionStats stats <> ["alias: " <> plain localAlias | localAlias /= ""] <> [viewConnectionVerified (memberSecurityCode m) | isJust stats] + <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn viewConnectionVerified :: Maybe SecurityCode -> StyledString viewConnectionVerified (Just _) = "connection verified" -- TODO show verification time? viewConnectionVerified _ = "connection not verified, use " <> highlight' "/code" <> " command to see security code" +viewPeerChatVRange :: JVersionRange -> StyledString +viewPeerChatVRange (JVersionRange (VersionRange minVer maxVer)) = "peer chat protocol version range: (" <> sShow minVer <> ", " <> sShow maxVer <> ")" + viewConnectionStats :: ConnectionStats -> [StyledString] viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] @@ -1023,10 +1088,11 @@ viewSwitchPhase = \case SPSecured -> "secured new address" SPCompleted -> "changed address" -viewUserProfileUpdated :: Profile -> Profile -> Int -> Int -> [StyledString] -viewUserProfileUpdated Profile {displayName = n, fullName, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', image = image', contactLink = contactLink', preferences = prefs'} s f = +viewUserProfileUpdated :: Profile -> Profile -> UserProfileUpdateSummary -> [StyledString] +viewUserProfileUpdated Profile {displayName = n, fullName, image, contactLink, preferences} Profile {displayName = n', fullName = fullName', image = image', contactLink = contactLink', preferences = prefs'} summary = profileUpdated <> viewPrefsUpdated preferences prefs' where + UserProfileUpdateSummary {updateSuccesses = s, updateFailures = f} = summary profileUpdated | n == n' && fullName == fullName' && image == image' && contactLink == contactLink' = [] | n == n' && fullName == fullName' && image == image' = [if isNothing contactLink' then "contact address removed" else "new contact address set"] @@ -1126,6 +1192,10 @@ viewGroupProfile g@GroupInfo {groupProfile = GroupProfile {description, image, g where pref = getGroupPreference f . mergeGroupPreferences +viewGroupDescription :: GroupInfo -> [StyledString] +viewGroupDescription GroupInfo {groupProfile = GroupProfile {description}} = + maybe ["No welcome message!"] ((bold' "Welcome message:" :) . map plain . T.lines) description + bold' :: String -> StyledString bold' = styled Bold @@ -1139,6 +1209,11 @@ viewConnectionAliasUpdated PendingContactConnection {pccConnId, localAlias} | localAlias == "" = ["connection " <> sShow pccConnId <> " alias removed"] | otherwise = ["connection " <> sShow pccConnId <> " alias updated: " <> plain localAlias] +viewConnectionIncognitoUpdated :: PendingContactConnection -> [StyledString] +viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserProfileId} + | isJust customUserProfileId = ["connection " <> sShow pccConnId <> " changed to incognito"] + | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] + viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated Contact {localDisplayName = n, profile = LocalProfile {fullName, contactLink}} @@ -1219,8 +1294,8 @@ viewSentBroadcast mc s f ts tz time = prependFirst (highlight' "/feed" <> " (" < | otherwise = "" viewSentFileInvitation :: StyledString -> CIFile d -> CurrentTime -> TimeZone -> CIMeta c d -> [StyledString] -viewSentFileInvitation to CIFile {fileId, filePath, fileStatus} ts tz = case filePath of - Just fPath -> sentWithTime_ ts tz $ ttySentFile fPath +viewSentFileInvitation to CIFile {fileId, fileSource, fileStatus} ts tz = case fileSource of + Just (CryptoFile fPath _) -> sentWithTime_ ts tz $ ttySentFile fPath _ -> const [] where ttySentFile fPath = ["/f " <> to <> ttyFilePath fPath] <> cancelSending @@ -1288,14 +1363,20 @@ humanReadableSize size mB = kB * 1024 gB = mB * 1024 -savingFile' :: AChatItem -> [StyledString] -savingFile' (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, filePath = Just filePath}, chatDir = CIDirectRcv}) = - ["saving file " <> sShow fileId <> " from " <> ttyContact c <> " to " <> plain filePath] -savingFile' (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, filePath = Just filePath}, chatDir = CIGroupRcv GroupMember {localDisplayName = m}}) = - ["saving file " <> sShow fileId <> " from " <> ttyContact m <> " to " <> plain filePath] -savingFile' (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, filePath = Just filePath}}) = - ["saving file " <> sShow fileId <> " to " <> plain filePath] -savingFile' _ = ["saving file"] -- shouldn't happen +savingFile' :: Bool -> AChatItem -> [StyledString] +savingFile' testView (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileSource = Just (CryptoFile filePath cfArgs_)}, chatDir}) = + let from = case (chat, chatDir) of + (DirectChat Contact {localDisplayName = c}, CIDirectRcv) -> " from " <> ttyContact c + (_, CIGroupRcv GroupMember {localDisplayName = m}) -> " from " <> ttyContact m + _ -> "" + in ["saving file " <> sShow fileId <> from <> " to " <> plain filePath] <> cfArgsStr + where + cfArgsStr = case cfArgs_ of + Just cfArgs@(CFArgs key nonce) + | testView -> [plain $ LB.unpack $ J.encode cfArgs] + | otherwise -> [plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce] + _ -> [] +savingFile' _ _ = ["saving file"] -- shouldn't happen receivingFile_' :: StyledString -> AChatItem -> [StyledString] receivingFile_' status (AChatItem _ _ (DirectChat Contact {localDisplayName = c}) ChatItem {file = Just CIFile {fileId, fileName}, chatDir = CIDirectRcv}) = @@ -1347,7 +1428,7 @@ viewFileTransferStatus (FTRcv ft@RcvFileTransfer {fileId, fileInvitation = FileI RFSCancelled Nothing -> "cancelled" viewFileTransferStatusXFTP :: AChatItem -> [StyledString] -viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, filePath}}) = +viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId, fileName, fileSize, fileStatus, fileSource}}) = case fileStatus of CIFSSndStored -> ["sending " <> fstr <> " just started"] CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize] @@ -1357,7 +1438,7 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] - CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\fp -> ", path: " <> plain fp) filePath] + CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] CIFSRcvError -> ["receiving " <> fstr <> " error"] CIFSInvalid text -> [fstr <> " invalid status: " <> plain text] @@ -1478,6 +1559,7 @@ viewChatError logLevel = \case <> (", connection id: " <> show connId) <> maybe "" (\MsgMetaJSON {rcvId} -> ", agent msg rcv id: " <> show rcvId) msgMeta_ ] + CEContactNotFound cName m_ -> viewContactNotFound cName m_ CEContactNotReady c -> [ttyContact' c <> ": not ready"] CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)] CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning] @@ -1504,8 +1586,8 @@ viewChatError logLevel = \case CEFileCancelled f -> ["file cancelled: " <> plain f] CEFileCancel fileId e -> ["error cancelling file " <> sShow fileId <> ": " <> sShow e] CEFileAlreadyExists f -> ["file already exists: " <> plain f] - CEFileRead f e -> ["cannot read file " <> plain f, sShow e] - CEFileWrite f e -> ["cannot write file " <> plain f, sShow e] + CEFileRead f e -> ["cannot read file " <> plain f <> ": " <> plain e] + CEFileWrite f e -> ["cannot write file " <> plain f <> ": " <> plain e] CEFileSend fileId e -> ["error sending file " <> sShow fileId <> ": " <> sShow e] CEFileRcvChunk e -> ["error receiving file: " <> plain e] CEFileInternal e -> ["file error: " <> plain e] @@ -1530,6 +1612,8 @@ viewChatError logLevel = \case CECommandError e -> ["bad chat command: " <> plain e] CEAgentCommandError e -> ["agent command error: " <> plain e] CEInvalidFileDescription e -> ["invalid file description: " <> plain e] + CEConnectionIncognitoChangeProhibited -> ["incognito mode change prohibited"] + CEPeerChatVRangeIncompatible -> ["peer chat protocol version range incompatible"] CEInternalError e -> ["internal chat error: " <> plain e] CEException e -> ["exception: " <> plain e] -- e -> ["chat error: " <> sShow e] diff --git a/stack.yaml b/stack.yaml index e96175d919..868e19faf5 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: fdbfe0e8d159f394f6eb0f5168620da8694033cd + commit: 53c793d5590d3c781aa3fbf72993eee262c7aa83 - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher diff --git a/tests/Bots/BroadcastTests.hs b/tests/Bots/BroadcastTests.hs new file mode 100644 index 0000000000..69ec10a7ab --- /dev/null +++ b/tests/Bots/BroadcastTests.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} + +module Bots.BroadcastTests where + +import Broadcast.Bot +import Broadcast.Options +import ChatClient +import ChatTests.Utils +import Control.Concurrent (forkIO, killThread, threadDelay) +import Control.Exception (bracket) +import Simplex.Chat.Bot.KnownContacts +import Simplex.Chat.Core +import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) +import Simplex.Chat.Types (Profile (..)) +import System.FilePath (()) +import Test.Hspec + +broadcastBotTests :: SpecWith FilePath +broadcastBotTests = do + it "should broadcast message" testBroadcastMessages + +withBroadcastBot :: BroadcastBotOpts -> IO () -> IO () +withBroadcastBot opts test = + bracket (forkIO bot) killThread (\_ -> threadDelay 500000 >> test) + where + bot = simplexChatCore testCfg (mkChatOpts opts) Nothing $ broadcastBot opts + +broadcastBotProfile :: Profile +broadcastBotProfile = Profile {displayName = "broadcast_bot", fullName = "Broadcast Bot", image = Nothing, contactLink = Nothing, preferences = Nothing} + +mkBotOpts :: FilePath -> [KnownContact] -> BroadcastBotOpts +mkBotOpts tmp publishers = + BroadcastBotOpts + { coreOptions = (coreOptions (testOpts :: ChatOpts)) {dbFilePrefix = tmp botDbPrefix}, + publishers, + welcomeMessage = defaultWelcomeMessage publishers, + prohibitedMessage = defaultWelcomeMessage publishers + } + +botDbPrefix :: FilePath +botDbPrefix = "broadcast_bot" + +testBroadcastMessages :: HasCallStack => FilePath -> IO () +testBroadcastMessages tmp = do + botLink <- + withNewTestChat tmp botDbPrefix broadcastBotProfile $ \bc_bot -> + withNewTestChat tmp "alice" aliceProfile $ \alice -> do + connectUsers bc_bot alice + bc_bot ##> "/ad" + getContactLink bc_bot True + let botOpts = mkBotOpts tmp [KnownContact 2 "alice"] + withBroadcastBot botOpts $ + withTestChat tmp "alice" $ \alice -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + alice <## "1 contacts connected (use /cs for the list)" + bob `connectVia` botLink + bob #> "@broadcast_bot hello" + bob <# "broadcast_bot> > hello" + bob <## " Hello! I am a broadcast bot." + bob <## "I broadcast messages to all connected users from @alice." + cath `connectVia` botLink + alice #> "@broadcast_bot hello all!" + bob <# "broadcast_bot> hello all!" + cath <# "broadcast_bot> hello all!" + alice <# "broadcast_bot> > hello all!" + alice <## " Forwarded to 2 contact(s)" + where + cc `connectVia` botLink = do + cc ##> ("/c " <> botLink) + cc <## "connection request sent!" + cc <## "broadcast_bot (Broadcast Bot): contact is connected" + cc <# "broadcast_bot> Hello! I am a broadcast bot." + cc <## "I broadcast messages to all connected users from @alice." diff --git a/tests/Bots/DirectoryTests.hs b/tests/Bots/DirectoryTests.hs new file mode 100644 index 0000000000..f34ab042e8 --- /dev/null +++ b/tests/Bots/DirectoryTests.hs @@ -0,0 +1,959 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PostfixOperators #-} + +module Bots.DirectoryTests where + +import ChatClient +import ChatTests.Utils +import Control.Concurrent (forkIO, killThread, threadDelay) +import Control.Exception (finally) +import Control.Monad (forM_) +import Directory.Options +import Directory.Service +import Directory.Store +import GHC.IO.Handle (hClose) +import Simplex.Chat.Bot.KnownContacts +import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Core +import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) +import Simplex.Chat.Types (GroupMemberRole (..), Profile (..)) +import System.FilePath (()) +import Test.Hspec + +directoryServiceTests :: SpecWith FilePath +directoryServiceTests = do + it "should register group" testDirectoryService + it "should suspend and resume group" testSuspendResume + it "should join found group via link" testJoinGroup + describe "de-listing the group" $ do + it "should de-list if owner leaves the group" testDelistedOwnerLeaves + it "should de-list if owner is removed from the group" testDelistedOwnerRemoved + it "should NOT de-list if another member leaves the group" testNotDelistedMemberLeaves + it "should NOT de-list if another member is removed from the group" testNotDelistedMemberRemoved + it "should de-list if service is removed from the group" testDelistedServiceRemoved + it "should de-list/re-list when service/owner roles change" testDelistedRoleChanges + it "should NOT de-list if another member role changes" testNotDelistedMemberRoleChanged + it "should NOT send to approval if roles are incorrect" testNotSentApprovalBadRoles + it "should NOT allow approving if roles are incorrect" testNotApprovedBadRoles + describe "should require re-approval if profile is changed by" $ do + it "the registration owner" testRegOwnerChangedProfile + it "another owner" testAnotherOwnerChangedProfile + describe "should require profile update if group link is removed by " $ do + it "the registration owner" testRegOwnerRemovedLink + it "another owner" testAnotherOwnerRemovedLink + describe "duplicate groups (same display name and full name)" $ do + it "should ask for confirmation if a duplicate group is submitted" testDuplicateAskConfirmation + it "should prohibit registration if a duplicate group is listed" testDuplicateProhibitRegistration + it "should prohibit confirmation if a duplicate group is listed" testDuplicateProhibitConfirmation + it "should prohibit when profile is updated and not send for approval" testDuplicateProhibitWhenUpdated + it "should prohibit approval if a duplicate group is listed" testDuplicateProhibitApproval + describe "list groups" $ do + it "should list user's groups" testListUserGroups + describe "store log" $ do + it "should restore directory service state" testRestoreDirectory + +directoryProfile :: Profile +directoryProfile = Profile {displayName = "SimpleX-Directory", fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} + +mkDirectoryOpts :: FilePath -> [KnownContact] -> DirectoryOpts +mkDirectoryOpts tmp superUsers = + DirectoryOpts + { coreOptions = (coreOptions (testOpts :: ChatOpts)) {dbFilePrefix = tmp serviceDbPrefix}, + superUsers, + directoryLog = Just $ tmp "directory_service.log", + serviceName = "SimpleX-Directory", + testing = True + } + +serviceDbPrefix :: FilePath +serviceDbPrefix = "directory_service" + +testDirectoryService :: HasCallStack => FilePath -> IO () +testDirectoryService tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + bob #> "@SimpleX-Directory privacy" + bob <# "SimpleX-Directory> > privacy" + bob <## " No groups found" + -- putStrLn "*** create a group" + bob ##> "/g PSA Privacy, Security & Anonymity" + bob <## "group #PSA (Privacy, Security & Anonymity) is created" + bob <## "to add members use /a PSA or /create link #PSA" + bob ##> "/a PSA SimpleX-Directory member" + bob <## "invitation to join the group #PSA sent to SimpleX-Directory" + bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" + bob ##> "/mr PSA SimpleX-Directory admin" + -- putStrLn "*** discover service joins group and creates the link for profile" + bob <## "#PSA: you changed the role of SimpleX-Directory from member to admin" + bob <# "SimpleX-Directory> Joining the group PSA…" + bob <## "#PSA: SimpleX-Directory joined the group" + bob <# "SimpleX-Directory> Joined the group PSA, creating the link…" + bob <# "SimpleX-Directory> Created the public link to join the group via this directory service that is always online." + bob <## "" + bob <## "Please add it to the group welcome message." + bob <## "For example, add:" + welcomeWithLink <- dropStrPrefix "SimpleX-Directory> " . dropTime <$> getTermLine bob + -- putStrLn "*** update profile without link" + updateGroupProfile bob "Welcome!" + bob <# "SimpleX-Directory> The profile updated for ID 1 (PSA), but the group link is not added to the welcome message." + (superUser Thank you! The group link for ID 1 (PSA) is added to the welcome message." + bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + approvalRequested superUser welcomeWithLink (1 :: Int) + -- putStrLn "*** update profile so that it still has link" + let welcomeWithLink' = "Welcome! " <> welcomeWithLink + updateGroupProfile bob welcomeWithLink' + bob <# "SimpleX-Directory> The group ID 1 (PSA) is updated!" + bob <## "It is hidden from the directory until approved." + superUser <# "SimpleX-Directory> The group ID 1 (PSA) is updated." + approvalRequested superUser welcomeWithLink' (2 :: Int) + -- putStrLn "*** try approving with the old registration code" + bob #> "@SimpleX-Directory /approve 1:PSA 1" + bob <# "SimpleX-Directory> > /approve 1:PSA 1" + bob <## " You are not allowed to use this command" + superUser #> "@SimpleX-Directory /approve 1:PSA 1" + superUser <# "SimpleX-Directory> > /approve 1:PSA 1" + superUser <## " Incorrect approval code" + -- putStrLn "*** update profile so that it has no link" + updateGroupProfile bob "Welcome!" + bob <# "SimpleX-Directory> The group link for ID 1 (PSA) is removed from the welcome message." + bob <## "" + bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." + superUser <# "SimpleX-Directory> The group link is removed from ID 1 (PSA), de-listed." + superUser #> "@SimpleX-Directory /approve 1:PSA 2" + superUser <# "SimpleX-Directory> > /approve 1:PSA 2" + superUser <## " Error: the group ID 1 (PSA) is not pending approval." + -- putStrLn "*** update profile so that it has link again" + updateGroupProfile bob welcomeWithLink' + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (PSA) is added to the welcome message." + bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + approvalRequested superUser welcomeWithLink' (1 :: Int) + superUser #> "@SimpleX-Directory /approve 1:PSA 1" + superUser <# "SimpleX-Directory> > /approve 1:PSA 1" + superUser <## " Group approved!" + bob <# "SimpleX-Directory> The group ID 1 (PSA) is approved and listed in directory!" + bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + search bob "privacy" welcomeWithLink' + search bob "security" welcomeWithLink' + cath `connectVia` dsLink + search cath "privacy" welcomeWithLink' + bob #> "@SimpleX-Directory /exec /contacts" + bob <# "SimpleX-Directory> > /exec /contacts" + bob <## " You are not allowed to use this command" + superUser #> "@SimpleX-Directory /exec /contacts" + superUser <# "SimpleX-Directory> > /exec /contacts" + superUser <## " alice (Alice)" + superUser <## "bob (Bob)" + superUser <## "cath (Catherine)" + where + search u s welcome = do + u #> ("@SimpleX-Directory " <> s) + u <# ("SimpleX-Directory> > " <> s) + u <## " Found 1 group(s)" + u <# "SimpleX-Directory> PSA (Privacy, Security & Anonymity)" + u <## "Welcome message:" + u <## welcome + u <## "2 members" + updateGroupProfile u welcome = do + u ##> ("/set welcome #PSA " <> welcome) + u <## "description changed to:" + u <## welcome + approvalRequested su welcome grId = do + su <# "SimpleX-Directory> bob submitted the group ID 1:" + su <## "PSA (Privacy, Security & Anonymity)" + su <## "Welcome message:" + su <## welcome + su <## "2 members" + su <## "" + su <## "To approve send:" + su <# ("SimpleX-Directory> /approve 1:PSA " <> show grId) + +testSuspendResume :: HasCallStack => FilePath -> IO () +testSuspendResume tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + groupFound bob "privacy" + superUser #> "@SimpleX-Directory /suspend 1:privacy" + superUser <# "SimpleX-Directory> > /suspend 1:privacy" + superUser <## " Group suspended!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is suspended and hidden from directory. Please contact the administrators." + groupNotFound bob "privacy" + superUser #> "@SimpleX-Directory /resume 1:privacy" + superUser <# "SimpleX-Directory> > /resume 1:privacy" + superUser <## " Group listing resumed!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is listed in the directory again!" + groupFound bob "privacy" + +testJoinGroup :: HasCallStack => FilePath -> IO () +testJoinGroup tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat tmp "cath" cathProfile $ \cath -> + withNewTestChat tmp "dan" danProfile $ \dan -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + cath `connectVia` dsLink + cath #> "@SimpleX-Directory privacy" + cath <# "SimpleX-Directory> > privacy" + cath <## " Found 1 group(s)" + cath <# "SimpleX-Directory> privacy (Privacy)" + cath <## "Welcome message:" + welcomeMsg <- getTermLine cath + let groupLink = dropStrPrefix "Link to join the group privacy: " welcomeMsg + cath <## "2 members" + cath ##> ("/c " <> groupLink) + cath <## "connection request sent!" + cath <## "SimpleX-Directory_1: contact is connected" + cath <## "contact SimpleX-Directory_1 is merged into SimpleX-Directory" + cath <## "use @SimpleX-Directory to send messages" + cath <## "#privacy: you joined the group" + cath <# ("#privacy SimpleX-Directory> " <> welcomeMsg) + cath <## "#privacy: member bob (Bob) is connected" + bob <## "#privacy: SimpleX-Directory added cath (Catherine) to the group (connecting...)" + bob <## "#privacy: new member cath is connected" + bob ##> "/create link #privacy" + bobLink <- getGroupLink bob "privacy" GRMember True + dan ##> ("/c " <> bobLink) + dan <## "connection request sent!" + concurrentlyN_ + [ do + bob <## "dan (Daniel): accepting request to join group #privacy..." + bob <## "dan (Daniel): contact is connected" + bob <## "dan invited to group #privacy via your group link" + bob <## "#privacy: dan joined the group", + do + dan <## "bob (Bob): contact is connected" + dan <## "#privacy: you joined the group" + dan <# ("#privacy bob> " <> welcomeMsg) + dan + <### [ "#privacy: member SimpleX-Directory is connected", + "#privacy: member cath (Catherine) is connected" + ], + do + cath <## "#privacy: bob added dan (Daniel) to the group (connecting...)" + cath <## "#privacy: new member dan is connected" + ] + +testDelistedOwnerLeaves :: HasCallStack => FilePath -> IO () +testDelistedOwnerLeaves tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + leaveGroup "privacy" bob + cath <## "#privacy: bob left the group" + bob <# "SimpleX-Directory> You left the group ID 1 (privacy)." + bob <## "" + bob <## "The group is no longer listed in the directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group owner left)." + groupNotFound cath "privacy" + +testDelistedOwnerRemoved :: HasCallStack => FilePath -> IO () +testDelistedOwnerRemoved tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + removeMember "privacy" cath bob + bob <# "SimpleX-Directory> You are removed from the group ID 1 (privacy)." + bob <## "" + bob <## "The group is no longer listed in the directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (group owner is removed)." + groupNotFound cath "privacy" + +testNotDelistedMemberLeaves :: HasCallStack => FilePath -> IO () +testNotDelistedMemberLeaves tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + leaveGroup "privacy" cath + bob <## "#privacy: cath left the group" + (superUser FilePath -> IO () +testNotDelistedMemberRemoved tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + removeMember "privacy" bob cath + (superUser FilePath -> IO () +testDelistedServiceRemoved tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + bob ##> "/rm #privacy SimpleX-Directory" + bob <## "#privacy: you removed SimpleX-Directory from the group" + cath <## "#privacy: bob removed SimpleX-Directory from the group" + bob <# "SimpleX-Directory> SimpleX-Directory is removed from the group ID 1 (privacy)." + bob <## "" + bob <## "The group is no longer listed in the directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (directory service is removed)." + groupNotFound cath "privacy" + +testDelistedRoleChanges :: HasCallStack => FilePath -> IO () +testDelistedRoleChanges tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + groupFoundN 3 cath "privacy" + -- de-listed if service role changed + bob ##> "/mr privacy SimpleX-Directory member" + bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + cath <## "#privacy: bob changed the role of SimpleX-Directory from admin to member" + bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to member." + bob <## "" + bob <## "The group is no longer listed in the directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (SimpleX-Directory role is changed to member)." + groupNotFound cath "privacy" + -- re-listed if service role changed back without profile changes + cath ##> "/mr privacy SimpleX-Directory admin" + cath <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <## "#privacy: cath changed the role of SimpleX-Directory from member to admin" + bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." + bob <## "" + bob <## "The group is listed in the directory again." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is listed (SimpleX-Directory role is changed to admin)." + groupFoundN 3 cath "privacy" + -- de-listed if owner role changed + cath ##> "/mr privacy bob admin" + cath <## "#privacy: you changed the role of bob from owner to admin" + bob <## "#privacy: cath changed your role from owner to admin" + bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to admin." + bob <## "" + bob <## "The group is no longer listed in the directory." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is de-listed (user role is set to admin)." + groupNotFound cath "privacy" + -- re-listed if owner role changed back without profile changes + cath ##> "/mr privacy bob owner" + cath <## "#privacy: you changed the role of bob from admin to owner" + bob <## "#privacy: cath changed your role from admin to owner" + bob <# "SimpleX-Directory> Your role in the group ID 1 (privacy) is changed to owner." + bob <## "" + bob <## "The group is listed in the directory again." + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is listed (user role is set to owner)." + groupFoundN 3 cath "privacy" + +testNotDelistedMemberRoleChanged :: HasCallStack => FilePath -> IO () +testNotDelistedMemberRoleChanged tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + groupFoundN 3 cath "privacy" + bob ##> "/mr privacy cath member" + bob <## "#privacy: you changed the role of cath from owner to member" + cath <## "#privacy: bob changed your role from owner to member" + groupFoundN 3 cath "privacy" + +testNotSentApprovalBadRoles :: HasCallStack => FilePath -> IO () +testNotSentApprovalBadRoles tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + submitGroup bob "privacy" "Privacy" + welcomeWithLink <- groupAccepted bob "privacy" + bob ##> "/mr privacy SimpleX-Directory member" + bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + updateProfileWithLink bob "privacy" welcomeWithLink 1 + bob <# "SimpleX-Directory> You must grant directory service admin role to register the group" + bob ##> "/mr privacy SimpleX-Directory admin" + bob <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." + bob <## "" + bob <## "The group is submitted for approval." + notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 + groupNotFound cath "privacy" + approveRegistration superUser bob "privacy" 1 + groupFound cath "privacy" + +testNotApprovedBadRoles :: HasCallStack => FilePath -> IO () +testNotApprovedBadRoles tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + submitGroup bob "privacy" "Privacy" + welcomeWithLink <- groupAccepted bob "privacy" + updateProfileWithLink bob "privacy" welcomeWithLink 1 + notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 + bob ##> "/mr privacy SimpleX-Directory member" + bob <## "#privacy: you changed the role of SimpleX-Directory from admin to member" + let approve = "/approve 1:privacy 1" + superUser #> ("@SimpleX-Directory " <> approve) + superUser <# ("SimpleX-Directory> > " <> approve) + superUser <## " Group is not approved: user is not an owner." + groupNotFound cath "privacy" + bob ##> "/mr privacy SimpleX-Directory admin" + bob <## "#privacy: you changed the role of SimpleX-Directory from member to admin" + bob <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (privacy) is changed to admin." + bob <## "" + bob <## "The group is submitted for approval." + notifySuperUser superUser bob "privacy" "Privacy" welcomeWithLink 1 + approveRegistration superUser bob "privacy" 1 + groupFound cath "privacy" + +testRegOwnerChangedProfile :: HasCallStack => FilePath -> IO () +testRegOwnerChangedProfile tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + bob ##> "/gp privacy privacy Privacy and Security" + bob <## "full name changed to: Privacy and Security" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <## "It is hidden from the directory until approved." + cath <## "bob updated group #privacy:" + cath <## "full name changed to: Privacy and Security" + groupNotFound cath "privacy" + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated." + reapproveGroup 3 superUser bob + groupFoundN 3 cath "privacy" + +testAnotherOwnerChangedProfile :: HasCallStack => FilePath -> IO () +testAnotherOwnerChangedProfile tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + cath ##> "/gp privacy privacy Privacy and Security" + cath <## "full name changed to: Privacy and Security" + bob <## "cath updated group #privacy:" + bob <## "full name changed to: Privacy and Security" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is updated!" + bob <## "It is hidden from the directory until approved." + groupNotFound cath "privacy" + superUser <# "SimpleX-Directory> The group ID 1 (privacy) is updated." + reapproveGroup 3 superUser bob + groupFoundN 3 cath "privacy" + +testRegOwnerRemovedLink :: HasCallStack => FilePath -> IO () +testRegOwnerRemovedLink tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + bob ##> "/show welcome #privacy" + bob <## "Welcome message:" + welcomeWithLink <- getTermLine bob + bob ##> "/set welcome #privacy Welcome!" + bob <## "description changed to:" + bob <## "Welcome!" + bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message." + bob <## "" + bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." + cath <## "bob updated group #privacy:" + cath <## "description changed to:" + cath <## "Welcome!" + superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + groupNotFound cath "privacy" + bob ##> ("/set welcome #privacy " <> welcomeWithLink) + bob <## "description changed to:" + bob <## welcomeWithLink + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." + bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + cath <## "bob updated group #privacy:" + cath <## "description changed to:" + cath <## welcomeWithLink + reapproveGroup 3 superUser bob + groupFoundN 3 cath "privacy" + +testAnotherOwnerRemovedLink :: HasCallStack => FilePath -> IO () +testAnotherOwnerRemovedLink tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + addCathAsOwner bob cath + bob ##> "/show welcome #privacy" + bob <## "Welcome message:" + welcomeWithLink <- getTermLine bob + cath ##> "/set welcome #privacy Welcome!" + cath <## "description changed to:" + cath <## "Welcome!" + bob <## "cath updated group #privacy:" + bob <## "description changed to:" + bob <## "Welcome!" + bob <# "SimpleX-Directory> The group link for ID 1 (privacy) is removed from the welcome message." + bob <## "" + bob <## "The group is hidden from the directory until the group link is added and the group is re-approved." + superUser <# "SimpleX-Directory> The group link is removed from ID 1 (privacy), de-listed." + groupNotFound cath "privacy" + cath ##> ("/set welcome #privacy " <> welcomeWithLink) + cath <## "description changed to:" + cath <## welcomeWithLink + bob <## "cath updated group #privacy:" + bob <## "description changed to:" + bob <## welcomeWithLink + bob <# "SimpleX-Directory> The group link is added by another group member, your registration will not be processed." + bob <## "" + bob <## "Please update the group profile yourself." + bob ##> ("/set welcome #privacy " <> welcomeWithLink <> " - welcome!") + bob <## "description changed to:" + bob <## (welcomeWithLink <> " - welcome!") + bob <# "SimpleX-Directory> Thank you! The group link for ID 1 (privacy) is added to the welcome message." + bob <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + cath <## "bob updated group #privacy:" + cath <## "description changed to:" + cath <## (welcomeWithLink <> " - welcome!") + reapproveGroup 3 superUser bob + groupFoundN 3 cath "privacy" + +testDuplicateAskConfirmation :: HasCallStack => FilePath -> IO () +testDuplicateAskConfirmation tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + submitGroup bob "privacy" "Privacy" + _ <- groupAccepted bob "privacy" + cath `connectVia` dsLink + submitGroup cath "privacy" "Privacy" + cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <## "To confirm the registration, please send:" + cath <# "SimpleX-Directory> /confirm 1:privacy" + cath #> "@SimpleX-Directory /confirm 1:privacy" + welcomeWithLink <- groupAccepted cath "privacy" + groupNotFound bob "privacy" + completeRegistration superUser cath "privacy" "Privacy" welcomeWithLink 2 + groupFound bob "privacy" + +testDuplicateProhibitRegistration :: HasCallStack => FilePath -> IO () +testDuplicateProhibitRegistration tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + cath `connectVia` dsLink + groupFound cath "privacy" + _ <- submitGroup cath "privacy" "Privacy" + cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." + +testDuplicateProhibitConfirmation :: HasCallStack => FilePath -> IO () +testDuplicateProhibitConfirmation tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + submitGroup bob "privacy" "Privacy" + welcomeWithLink <- groupAccepted bob "privacy" + cath `connectVia` dsLink + submitGroup cath "privacy" "Privacy" + cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <## "To confirm the registration, please send:" + cath <# "SimpleX-Directory> /confirm 1:privacy" + groupNotFound cath "privacy" + completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 + groupFound cath "privacy" + cath #> "@SimpleX-Directory /confirm 1:privacy" + cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." + +testDuplicateProhibitWhenUpdated :: HasCallStack => FilePath -> IO () +testDuplicateProhibitWhenUpdated tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + submitGroup bob "privacy" "Privacy" + welcomeWithLink <- groupAccepted bob "privacy" + cath `connectVia` dsLink + submitGroup cath "privacy" "Privacy" + cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <## "To confirm the registration, please send:" + cath <# "SimpleX-Directory> /confirm 1:privacy" + cath #> "@SimpleX-Directory /confirm 1:privacy" + welcomeWithLink' <- groupAccepted cath "privacy" + groupNotFound cath "privacy" + completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 + groupFound cath "privacy" + cath ##> ("/set welcome privacy " <> welcomeWithLink') + cath <## "description changed to:" + cath <## welcomeWithLink' + cath <# "SimpleX-Directory> The group privacy (Privacy) is already listed in the directory, please choose another name." + cath ##> "/gp privacy security Security" + cath <## "changed to #security (Security)" + cath <# "SimpleX-Directory> Thank you! The group link for ID 2 (security) is added to the welcome message." + cath <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + notifySuperUser superUser cath "security" "Security" welcomeWithLink' 2 + approveRegistration superUser cath "security" 2 + groupFound bob "security" + groupFound cath "security" + +testDuplicateProhibitApproval :: HasCallStack => FilePath -> IO () +testDuplicateProhibitApproval tmp = + withDirectoryService tmp $ \superUser dsLink -> + withNewTestChat tmp "bob" bobProfile $ \bob -> + withNewTestChat tmp "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + submitGroup bob "privacy" "Privacy" + welcomeWithLink <- groupAccepted bob "privacy" + cath `connectVia` dsLink + submitGroup cath "privacy" "Privacy" + cath <# "SimpleX-Directory> The group privacy (Privacy) is already submitted to the directory." + cath <## "To confirm the registration, please send:" + cath <# "SimpleX-Directory> /confirm 1:privacy" + cath #> "@SimpleX-Directory /confirm 1:privacy" + welcomeWithLink' <- groupAccepted cath "privacy" + updateProfileWithLink cath "privacy" welcomeWithLink' 2 + notifySuperUser superUser cath "privacy" "Privacy" welcomeWithLink' 2 + groupNotFound cath "privacy" + completeRegistration superUser bob "privacy" "Privacy" welcomeWithLink 1 + groupFound cath "privacy" + -- fails at approval, as already listed + let approve = "/approve 2:privacy 1" + superUser #> ("@SimpleX-Directory " <> approve) + superUser <# ("SimpleX-Directory> > " <> approve) + superUser <## " The group ID 2 (privacy) is already listed in the directory." + +testListUserGroups :: HasCallStack => FilePath -> IO () +testListUserGroups tmp = + withDirectoryServiceCfg tmp testCfgCreateGroupDirect $ \superUser dsLink -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "bob" bobProfile $ \bob -> + withNewTestChatCfg tmp testCfgCreateGroupDirect "cath" cathProfile $ \cath -> do + bob `connectVia` dsLink + cath `connectVia` dsLink + registerGroup superUser bob "privacy" "Privacy" + connectUsers bob cath + fullAddMember "privacy" "Privacy" bob cath GRMember + joinGroup "privacy" cath bob + cath <## "#privacy: member SimpleX-Directory_1 is connected" + cath <## "contact SimpleX-Directory_1 is merged into SimpleX-Directory" + cath <## "use @SimpleX-Directory to send messages" + registerGroupId superUser bob "security" "Security" 2 2 + registerGroupId superUser cath "anonymity" "Anonymity" 3 1 + cath #> "@SimpleX-Directory /list" + cath <# "SimpleX-Directory> > /list" + cath <## " 1 registered group(s)" + cath <# "SimpleX-Directory> 1. anonymity (Anonymity)" + cath <## "Welcome message:" + cath <##. "Link to join the group anonymity: " + cath <## "2 members" + cath <## "Status: active" + -- with de-listed group + groupFound cath "anonymity" + cath ##> "/mr anonymity SimpleX-Directory member" + cath <## "#anonymity: you changed the role of SimpleX-Directory from admin to member" + cath <# "SimpleX-Directory> SimpleX-Directory role in the group ID 1 (anonymity) is changed to member." + cath <## "" + cath <## "The group is no longer listed in the directory." + superUser <# "SimpleX-Directory> The group ID 3 (anonymity) is de-listed (SimpleX-Directory role is changed to member)." + groupNotFound cath "anonymity" + listGroups superUser bob cath + +testRestoreDirectory :: HasCallStack => FilePath -> IO () +testRestoreDirectory tmp = do + testListUserGroups tmp + restoreDirectoryService tmp 3 3 $ \superUser _dsLink -> + withTestChat tmp "bob" $ \bob -> + withTestChat tmp "cath" $ \cath -> do + bob <## "2 contacts connected (use /cs for the list)" + bob + <### [ "#privacy (Privacy): connected to server(s)", + "#security (Security): connected to server(s)" + ] + cath <## "2 contacts connected (use /cs for the list)" + cath + <### [ "#privacy (Privacy): connected to server(s)", + "#anonymity (Anonymity): connected to server(s)" + ] + listGroups superUser bob cath + groupFoundN 3 bob "privacy" + groupFound bob "security" + groupFoundN 3 cath "privacy" + groupFound cath "security" + +listGroups :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () +listGroups superUser bob cath = do + bob #> "@SimpleX-Directory /list" + bob <# "SimpleX-Directory> > /list" + bob <## " 2 registered group(s)" + bob <# "SimpleX-Directory> 1. privacy (Privacy)" + bob <## "Welcome message:" + bob <##. "Link to join the group privacy: " + bob <## "3 members" + bob <## "Status: active" + bob <# "SimpleX-Directory> 2. security (Security)" + bob <## "Welcome message:" + bob <##. "Link to join the group security: " + bob <## "2 members" + bob <## "Status: active" + cath #> "@SimpleX-Directory /list" + cath <# "SimpleX-Directory> > /list" + cath <## " 1 registered group(s)" + cath <# "SimpleX-Directory> 1. anonymity (Anonymity)" + cath <## "Welcome message:" + cath <##. "Link to join the group anonymity: " + cath <## "2 members" + cath <## "Status: suspended because roles changed" + -- superuser lists all groups + bob #> "@SimpleX-Directory /last" + bob <# "SimpleX-Directory> > /last" + bob <## " You are not allowed to use this command" + superUser #> "@SimpleX-Directory /last" + superUser <# "SimpleX-Directory> > /last" + superUser <## " 3 registered group(s)" + superUser <# "SimpleX-Directory> 1. privacy (Privacy)" + superUser <## "Welcome message:" + superUser <##. "Link to join the group privacy: " + superUser <## "Owner: bob" + superUser <## "3 members" + superUser <## "Status: active" + superUser <# "SimpleX-Directory> 2. security (Security)" + superUser <## "Welcome message:" + superUser <##. "Link to join the group security: " + superUser <## "Owner: bob" + superUser <## "2 members" + superUser <## "Status: active" + superUser <# "SimpleX-Directory> 3. anonymity (Anonymity)" + superUser <## "Welcome message:" + superUser <##. "Link to join the group anonymity: " + superUser <## "Owner: cath" + superUser <## "2 members" + superUser <## "Status: suspended because roles changed" + -- showing last 1 group + superUser #> "@SimpleX-Directory /last 1" + superUser <# "SimpleX-Directory> > /last 1" + superUser <## " 3 registered group(s), showing the last 1" + superUser <# "SimpleX-Directory> 3. anonymity (Anonymity)" + superUser <## "Welcome message:" + superUser <##. "Link to join the group anonymity: " + superUser <## "Owner: cath" + superUser <## "2 members" + superUser <## "Status: suspended because roles changed" + +reapproveGroup :: HasCallStack => Int -> TestCC -> TestCC -> IO () +reapproveGroup count superUser bob = do + superUser <# "SimpleX-Directory> bob submitted the group ID 1:" + superUser <##. "privacy (" + superUser <## "Welcome message:" + superUser <##. "Link to join the group privacy: " + superUser <## (show count <> " members") + superUser <## "" + superUser <## "To approve send:" + superUser <# "SimpleX-Directory> /approve 1:privacy 1" + superUser #> "@SimpleX-Directory /approve 1:privacy 1" + superUser <# "SimpleX-Directory> > /approve 1:privacy 1" + superUser <## " Group approved!" + bob <# "SimpleX-Directory> The group ID 1 (privacy) is approved and listed in directory!" + bob <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + +addCathAsOwner :: HasCallStack => TestCC -> TestCC -> IO () +addCathAsOwner bob cath = do + connectUsers bob cath + fullAddMember "privacy" "Privacy" bob cath GROwner + joinGroup "privacy" cath bob + cath <## "#privacy: member SimpleX-Directory is connected" + +withDirectoryService :: HasCallStack => FilePath -> (TestCC -> String -> IO ()) -> IO () +withDirectoryService tmp = withDirectoryServiceCfg tmp testCfg + +withDirectoryServiceCfg :: HasCallStack => FilePath -> ChatConfig -> (TestCC -> String -> IO ()) -> IO () +withDirectoryServiceCfg tmp cfg test = do + dsLink <- + withNewTestChatCfg tmp cfg serviceDbPrefix directoryProfile $ \ds -> + withNewTestChatCfg tmp cfg "super_user" aliceProfile $ \superUser -> do + connectUsers ds superUser + ds ##> "/ad" + getContactLink ds True + withDirectory tmp cfg dsLink test + +restoreDirectoryService :: HasCallStack => FilePath -> Int -> Int -> (TestCC -> String -> IO ()) -> IO () +restoreDirectoryService tmp ctCount grCount test = do + dsLink <- + withTestChat tmp serviceDbPrefix $ \ds -> do + ds <## (show ctCount <> " contacts connected (use /cs for the list)") + ds <## "Your address is active! To show: /sa" + ds <## (show grCount <> " group links active") + forM_ [1 .. grCount] $ \_ -> ds <##. "#" + ds ##> "/sa" + dsLink <- getContactLink ds False + ds <## "auto_accept on" + pure dsLink + withDirectory tmp testCfg dsLink test + +withDirectory :: HasCallStack => FilePath -> ChatConfig -> String -> (TestCC -> String -> IO ()) -> IO () +withDirectory tmp cfg dsLink test = do + let opts = mkDirectoryOpts tmp [KnownContact 2 "alice"] + runDirectory cfg opts $ + withTestChatCfg tmp cfg "super_user" $ \superUser -> do + superUser <## "1 contacts connected (use /cs for the list)" + test superUser dsLink + +runDirectory :: ChatConfig -> DirectoryOpts -> IO () -> IO () +runDirectory cfg opts@DirectoryOpts {directoryLog} action = do + st <- restoreDirectoryStore directoryLog + t <- forkIO $ bot st + threadDelay 500000 + action `finally` (mapM_ hClose (directoryLogFile st) >> killThread t) + where + bot st = simplexChatCore cfg (mkChatOpts opts) Nothing $ directoryService st opts + +registerGroup :: TestCC -> TestCC -> String -> String -> IO () +registerGroup su u n fn = registerGroupId su u n fn 1 1 + +registerGroupId :: TestCC -> TestCC -> String -> String -> Int -> Int -> IO () +registerGroupId su u n fn gId ugId = do + submitGroup u n fn + welcomeWithLink <- groupAccepted u n + completeRegistrationId su u n fn welcomeWithLink gId ugId + +submitGroup :: TestCC -> String -> String -> IO () +submitGroup u n fn = do + u ##> ("/g " <> n <> " " <> fn) + u <## ("group #" <> n <> " (" <> fn <> ") is created") + u <## ("to add members use /a " <> n <> " or /create link #" <> n) + u ##> ("/a " <> n <> " SimpleX-Directory admin") + u <## ("invitation to join the group #" <> n <> " sent to SimpleX-Directory") + +groupAccepted :: TestCC -> String -> IO String +groupAccepted u n = do + u <# ("SimpleX-Directory> Joining the group " <> n <> "…") + u <## ("#" <> n <> ": SimpleX-Directory joined the group") + u <# ("SimpleX-Directory> Joined the group " <> n <> ", creating the link…") + u <# "SimpleX-Directory> Created the public link to join the group via this directory service that is always online." + u <## "" + u <## "Please add it to the group welcome message." + u <## "For example, add:" + dropStrPrefix "SimpleX-Directory> " . dropTime <$> getTermLine u -- welcome message with link + +completeRegistration :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () +completeRegistration su u n fn welcomeWithLink gId = + completeRegistrationId su u n fn welcomeWithLink gId gId + +completeRegistrationId :: TestCC -> TestCC -> String -> String -> String -> Int -> Int -> IO () +completeRegistrationId su u n fn welcomeWithLink gId ugId = do + updateProfileWithLink u n welcomeWithLink ugId + notifySuperUser su u n fn welcomeWithLink gId + approveRegistrationId su u n gId ugId + +updateProfileWithLink :: TestCC -> String -> String -> Int -> IO () +updateProfileWithLink u n welcomeWithLink ugId = do + u ##> ("/set welcome " <> n <> " " <> welcomeWithLink) + u <## "description changed to:" + u <## welcomeWithLink + u <# ("SimpleX-Directory> Thank you! The group link for ID " <> show ugId <> " (" <> n <> ") is added to the welcome message.") + u <## "You will be notified once the group is added to the directory - it may take up to 24 hours." + +notifySuperUser :: TestCC -> TestCC -> String -> String -> String -> Int -> IO () +notifySuperUser su u n fn welcomeWithLink gId = do + uName <- userName u + su <# ("SimpleX-Directory> " <> uName <> " submitted the group ID " <> show gId <> ":") + su <## (n <> " (" <> fn <> ")") + su <## "Welcome message:" + su <## welcomeWithLink + su .<## "members" + su <## "" + su <## "To approve send:" + let approve = "/approve " <> show gId <> ":" <> n <> " 1" + su <# ("SimpleX-Directory> " <> approve) + +approveRegistration :: TestCC -> TestCC -> String -> Int -> IO () +approveRegistration su u n gId = + approveRegistrationId su u n gId gId + +approveRegistrationId :: TestCC -> TestCC -> String -> Int -> Int -> IO () +approveRegistrationId su u n gId ugId = do + let approve = "/approve " <> show gId <> ":" <> n <> " 1" + su #> ("@SimpleX-Directory " <> approve) + su <# ("SimpleX-Directory> > " <> approve) + su <## " Group approved!" + u <# ("SimpleX-Directory> The group ID " <> show ugId <> " (" <> n <> ") is approved and listed in directory!") + u <## "Please note: if you change the group profile it will be hidden from directory until it is re-approved." + +connectVia :: TestCC -> String -> IO () +u `connectVia` dsLink = do + u ##> ("/c " <> dsLink) + u <## "connection request sent!" + u <## "SimpleX-Directory: contact is connected" + u <# "SimpleX-Directory> Welcome to SimpleX-Directory service!" + u <## "Send a search string to find groups or /help to learn how to add groups to directory." + u <## "" + u <## "For example, send privacy to find groups about privacy." + u <## "" + u <## "Content and privacy policy: https://simplex.chat/docs/directory.html" + +joinGroup :: String -> TestCC -> TestCC -> IO () +joinGroup gName member host = do + let gn = "#" <> gName + memberName <- userName member + hostName <- userName host + member ##> ("/j " <> gName) + member <## (gn <> ": you joined the group") + member <#. (gn <> " " <> hostName <> "> Link to join the group " <> gName <> ": ") + host <## (gn <> ": " <> memberName <> " joined the group") + +leaveGroup :: String -> TestCC -> IO () +leaveGroup gName member = do + let gn = "#" <> gName + member ##> ("/l " <> gName) + member <## (gn <> ": you left the group") + member <## ("use /d " <> gn <> " to delete the group") + +removeMember :: String -> TestCC -> TestCC -> IO () +removeMember gName admin removed = do + let gn = "#" <> gName + adminName <- userName admin + removedName <- userName removed + admin ##> ("/rm " <> gName <> " " <> removedName) + admin <## (gn <> ": you removed " <> removedName <> " from the group") + removed <## (gn <> ": " <> adminName <> " removed you from the group") + removed <## ("use /d " <> gn <> " to delete the group") + +groupFound :: TestCC -> String -> IO () +groupFound = groupFoundN 2 + +groupFoundN :: Int -> TestCC -> String -> IO () +groupFoundN count u name = do + u #> ("@SimpleX-Directory " <> name) + u <# ("SimpleX-Directory> > " <> name) + u <## " Found 1 group(s)" + u <#. ("SimpleX-Directory> " <> name <> " (") + u <## "Welcome message:" + u <##. "Link to join the group " + u <## (show count <> " members") + +groupNotFound :: TestCC -> String -> IO () +groupNotFound u s = do + u #> ("@SimpleX-Directory " <> s) + u <# ("SimpleX-Directory> > " <> s) + u <## " No groups found" diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 85ef243191..de6353d2e0 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -133,6 +133,16 @@ testAgentCfgV1 = testCfgV1 :: ChatConfig testCfgV1 = testCfg {agentConfig = testAgentCfgV1} +testCfgCreateGroupDirect :: ChatConfig +testCfgCreateGroupDirect = + mkCfgCreateGroupDirect testCfg + +mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig +mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange} + +groupCreateDirectVRange :: VersionRange +groupCreateDirectVRange = mkVersionRange 1 1 + createTestChat :: FilePath -> ChatConfig -> ChatOpts -> String -> Profile -> IO TestCC createTestChat tmp cfg opts@ChatOpts {coreOptions = CoreChatOpts {dbKey}} dbPrefix profile = do Right db@ChatDatabase {chatStore} <- createChatDatabase (tmp dbPrefix) dbKey MCError @@ -270,7 +280,7 @@ testChatOpts2 = testChatCfgOpts2 testCfg testChatCfgOpts2 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> IO ()) -> FilePath -> IO () testChatCfgOpts2 cfg opts p1 p2 test = testChatN cfg opts [p1, p2] test_ where - test_ :: [TestCC] -> IO () + test_ :: HasCallStack => [TestCC] -> IO () test_ [tc1, tc2] = test tc1 tc2 test_ _ = error "expected 2 chat clients" @@ -283,14 +293,17 @@ testChatCfg3 cfg = testChatCfgOpts3 cfg testOpts testChatCfgOpts3 :: HasCallStack => ChatConfig -> ChatOpts -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () testChatCfgOpts3 cfg opts p1 p2 p3 test = testChatN cfg opts [p1, p2, p3] test_ where - test_ :: [TestCC] -> IO () + test_ :: HasCallStack => [TestCC] -> IO () test_ [tc1, tc2, tc3] = test tc1 tc2 tc3 test_ _ = error "expected 3 chat clients" testChat4 :: HasCallStack => Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () -testChat4 p1 p2 p3 p4 test = testChatN testCfg testOpts [p1, p2, p3, p4] test_ +testChat4 = testChatCfg4 testCfg + +testChatCfg4 :: HasCallStack => ChatConfig -> Profile -> Profile -> Profile -> Profile -> (HasCallStack => TestCC -> TestCC -> TestCC -> TestCC -> IO ()) -> FilePath -> IO () +testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test_ where - test_ :: [TestCC] -> IO () + test_ :: HasCallStack => [TestCC] -> IO () test_ [tc1, tc2, tc3, tc4] = test tc1 tc2 tc3 tc4 test_ _ = error "expected 4 chat clients" diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index ed81853ac8..eeb96503e3 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -8,7 +8,7 @@ import Test.Hspec chatTests :: SpecWith FilePath chatTests = do - chatDirectTests - chatGroupTests - chatFileTests - chatProfileTests + describe "direct tests" chatDirectTests + describe "group tests" chatGroupTests + describe "file tests" chatFileTests + describe "profile tests" chatProfileTests diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 7d4774500f..3db4052226 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -17,9 +17,11 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Simplex.Chat.Call import Simplex.Chat.Controller (ChatConfig (..)) import Simplex.Chat.Options (ChatOpts (..)) +import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (authErrDisableCount, sameVerificationCode, verificationCode) import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Version import System.Directory (copyFile, doesDirectoryExist, doesFileExist) import System.FilePath (()) import Test.Hspec @@ -55,6 +57,8 @@ chatDirectTests = do it "start/stop/export/import chat" testMaintenanceMode it "export/import chat with files" testMaintenanceModeWithFiles it "encrypt/decrypt database" testDatabaseEncryption + describe "coordination between app and NSE" $ do + it "should not subscribe in NSE and subscribe in the app" testSubscribeAppNSE describe "mute/unmute messages" $ do it "mute/unmute contact" testMuteContact it "mute/unmute group" testMuteGroup @@ -94,6 +98,21 @@ chatDirectTests = do describe "delivery receipts" $ do it "should send delivery receipts" testSendDeliveryReceipts it "should send delivery receipts depending on configuration" testConfigureDeliveryReceipts + describe "negotiate connection peer chat protocol version range" $ do + describe "peer version range correctly set for new connection via invitation" $ do + testInvVRange supportedChatVRange supportedChatVRange + testInvVRange supportedChatVRange vr11 + testInvVRange vr11 supportedChatVRange + testInvVRange vr11 vr11 + describe "peer version range correctly set for new connection via contact request" $ do + testReqVRange supportedChatVRange supportedChatVRange + testReqVRange supportedChatVRange vr11 + testReqVRange vr11 supportedChatVRange + testReqVRange vr11 vr11 + it "update peer version range on received messages" testUpdatePeerChatVRange + where + testInvVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnInvChatVRange vr1 vr2 + testReqVRange vr1 vr2 = it (vRangeStr vr1 <> " - " <> vRangeStr vr2) $ testConnReqChatVRange vr1 vr2 testAddContact :: HasCallStack => SpecWith FilePath testAddContact = versionTestMatrix2 runTestAddContact @@ -953,6 +972,35 @@ testDatabaseEncryption tmp = do withTestChat tmp "alice" $ \alice -> do testChatWorking alice bob +testSubscribeAppNSE :: HasCallStack => FilePath -> IO () +testSubscribeAppNSE tmp = + withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat tmp "alice" aliceProfile $ \alice -> do + withTestChatOpts tmp testOpts {maintenance = True} "alice" $ \nseAlice -> do + alice ##> "/_app suspend 1" + alice <## "ok" + alice <## "chat suspended" + nseAlice ##> "/_start subscribe=off expire=off xftp=off" + nseAlice <## "chat started" + nseAlice ##> "/ad" + cLink <- getContactLink nseAlice True + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + (nseAlice "/_app activate" + alice <## "ok" + alice <## "Your address is active! To show: /sa" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + threadDelay 100000 + alice <##> bob + testMuteContact :: HasCallStack => FilePath -> IO () testMuteContact = testChat2 aliceProfile bobProfile $ @@ -990,7 +1038,7 @@ testMuteGroup = (bob hi") bob ##> "/gs" - bob <## "#team (muted, you can /unmute #team)" + bob <## "#team (3 members, muted, you can /unmute #team)" bob ##> "/unmute #team" bob <## "ok" alice #> "#team hi again" @@ -998,7 +1046,7 @@ testMuteGroup = (bob <# "#team alice> hi again") (cath <# "#team alice> hi again") bob ##> "/gs" - bob <## "#team" + bob <## "#team (3 members)" testCreateSecondUser :: HasCallStack => FilePath -> IO () testCreateSecondUser = @@ -1939,8 +1987,7 @@ testMarkContactVerified = testChat2 aliceProfile bobProfile $ \alice bob -> do connectUsers alice bob alice ##> "/i bob" - bobInfo alice - alice <## "connection not verified, use /code command to see security code" + bobInfo alice False alice ##> "/code bob" bCode <- getTermLine alice bob ##> "/code alice" @@ -1951,28 +1998,31 @@ testMarkContactVerified = alice ##> ("/verify bob " <> aCode) alice <## "connection verified" alice ##> "/i bob" - bobInfo alice - alice <## "connection verified" + bobInfo alice True alice ##> "/verify bob" alice <##. "connection not verified, current code is " alice ##> "/i bob" - bobInfo alice - alice <## "connection not verified, use /code command to see security code" + bobInfo alice False where - bobInfo :: HasCallStack => TestCC -> IO () - bobInfo alice = do + bobInfo :: HasCallStack => TestCC -> Bool -> IO () + bobInfo alice verified = do alice <## "contact ID: 2" alice <## "receiving messages via: localhost" alice <## "sending messages via: localhost" alice <## "you've shared main profile with this contact" + alice <## connVerified + alice <## currentChatVRangeInfo + where + connVerified + | verified = "connection verified" + | otherwise = "connection not verified, use /code command to see security code" testMarkGroupMemberVerified :: HasCallStack => FilePath -> IO () testMarkGroupMemberVerified = testChat2 aliceProfile bobProfile $ \alice bob -> do createGroup2 "team" alice bob alice ##> "/i #team bob" - bobInfo alice - alice <## "connection not verified, use /code command to see security code" + bobInfo alice False alice ##> "/code #team bob" bCode <- getTermLine alice bob ##> "/code #team alice" @@ -1983,20 +2033,24 @@ testMarkGroupMemberVerified = alice ##> ("/verify #team bob " <> aCode) alice <## "connection verified" alice ##> "/i #team bob" - bobInfo alice - alice <## "connection verified" + bobInfo alice True alice ##> "/verify #team bob" alice <##. "connection not verified, current code is " alice ##> "/i #team bob" - bobInfo alice - alice <## "connection not verified, use /code command to see security code" + bobInfo alice False where - bobInfo :: HasCallStack => TestCC -> IO () - bobInfo alice = do + bobInfo :: HasCallStack => TestCC -> Bool -> IO () + bobInfo alice verified = do alice <## "group ID: 1" alice <## "member ID: 2" alice <## "receiving messages via: localhost" alice <## "sending messages via: localhost" + alice <## connVerified + alice <## currentChatVRangeInfo + where + connVerified + | verified = "connection verified" + | otherwise = "connection not verified, use /code command to see security code" testMsgDecryptError :: HasCallStack => FilePath -> IO () testMsgDecryptError tmp = @@ -2088,8 +2142,7 @@ testSyncRatchetCodeReset tmp = alice <# "bob> hey" -- connection not verified bob ##> "/i alice" - aliceInfo bob - bob <## "connection not verified, use /code command to see security code" + aliceInfo bob False -- verify connection alice ##> "/code bob" bCode <- getTermLine alice @@ -2097,8 +2150,7 @@ testSyncRatchetCodeReset tmp = bob <## "connection verified" -- connection verified bob ##> "/i alice" - aliceInfo bob - bob <## "connection verified" + aliceInfo bob True setupDesynchronizedRatchet tmp alice withTestChat tmp "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" @@ -2115,20 +2167,25 @@ testSyncRatchetCodeReset tmp = -- connection not verified bob ##> "/i alice" - aliceInfo bob - bob <## "connection not verified, use /code command to see security code" + aliceInfo bob False alice #> "@bob hello again" bob <# "alice> hello again" bob #> "@alice received!" alice <# "bob> received!" where - aliceInfo :: HasCallStack => TestCC -> IO () - aliceInfo bob = do + aliceInfo :: HasCallStack => TestCC -> Bool -> IO () + aliceInfo bob verified = do bob <## "contact ID: 2" bob <## "receiving messages via: localhost" bob <## "sending messages via: localhost" bob <## "you've shared main profile with this contact" + bob <## connVerified + bob <## currentChatVRangeInfo + where + connVerified + | verified = "connection verified" + | otherwise = "connection not verified, use /code command to see security code" testSetMessageReactions :: HasCallStack => FilePath -> IO () testSetMessageReactions = @@ -2213,13 +2270,13 @@ testConfigureDeliveryReceipts tmp = noReceipt cath alice "4" -- configure receipts for user contacts - alice ##> "/_set receipts 1 on" + alice ##> "/_set receipts contacts 1 on" alice <## "ok" receipt bob alice "5" receipt cath alice "6" -- configure receipts for user contacts (terminal api) - alice ##> "/set receipts off" + alice ##> "/set receipts contacts off" alice <## "ok" noReceipt bob alice "7" noReceipt cath alice "8" @@ -2231,18 +2288,18 @@ testConfigureDeliveryReceipts tmp = noReceipt cath alice "10" -- configure receipts for user contacts (don't clear overrides) - alice ##> "/_set receipts 1 off" + alice ##> "/_set receipts contacts 1 off" alice <## "ok" receipt bob alice "11" noReceipt cath alice "12" - alice ##> "/_set receipts 1 off clear_overrides=off" + alice ##> "/_set receipts contacts 1 off clear_overrides=off" alice <## "ok" receipt bob alice "13" noReceipt cath alice "14" -- configure receipts for user contacts (clear overrides) - alice ##> "/set receipts off clear_overrides=on" + alice ##> "/set receipts contacts off clear_overrides=on" alice <## "ok" noReceipt bob alice "15" noReceipt cath alice "16" @@ -2271,3 +2328,85 @@ testConfigureDeliveryReceipts tmp = cc1 #> ("@" <> name2 <> " " <> msg) cc2 <# (name1 <> "> " <> msg) cc1 VersionRange -> VersionRange -> FilePath -> IO () +testConnInvChatVRange ct1VRange ct2VRange tmp = + withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do + connectUsers alice bob + + alice ##> "/i bob" + contactInfoChatVRange alice ct2VRange + + bob ##> "/i alice" + contactInfoChatVRange bob ct1VRange + +testConnReqChatVRange :: HasCallStack => VersionRange -> VersionRange -> FilePath -> IO () +testConnReqChatVRange ct1VRange ct2VRange tmp = + withNewTestChatCfg tmp testCfg {chatVRange = ct1VRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp testCfg {chatVRange = ct2VRange} "bob" bobProfile $ \bob -> do + alice ##> "/ad" + cLink <- getContactLink alice True + bob ##> ("/c " <> cLink) + alice <#? bob + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + + alice ##> "/i bob" + contactInfoChatVRange alice ct2VRange + + bob ##> "/i alice" + contactInfoChatVRange bob ct1VRange + +testUpdatePeerChatVRange :: HasCallStack => FilePath -> IO () +testUpdatePeerChatVRange tmp = + withNewTestChat tmp "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp cfg11 "bob" bobProfile $ \bob -> do + connectUsers alice bob + + alice ##> "/i bob" + contactInfoChatVRange alice vr11 + + bob ##> "/i alice" + contactInfoChatVRange bob supportedChatVRange + + withTestChat tmp "bob" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + + bob #> "@alice hello 1" + alice <# "bob> hello 1" + + alice ##> "/i bob" + contactInfoChatVRange alice supportedChatVRange + + bob ##> "/i alice" + contactInfoChatVRange bob supportedChatVRange + + withTestChatCfg tmp cfg11 "bob" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + + bob #> "@alice hello 2" + alice <# "bob> hello 2" + + alice ##> "/i bob" + contactInfoChatVRange alice vr11 + + bob ##> "/i alice" + contactInfoChatVRange bob supportedChatVRange + where + cfg11 = testCfg {chatVRange = vr11} :: ChatConfig + +vr11 :: VersionRange +vr11 = mkVersionRange 1 1 + +contactInfoChatVRange :: TestCC -> VersionRange -> IO () +contactInfoChatVRange cc (VersionRange minVer maxVer) = do + cc <## "contact ID: 2" + cc <## "receiving messages via: localhost" + cc <## "sending messages via: localhost" + cc <## "you've shared main profile with this contact" + cc <## "connection not verified, use /code command to see security code" + cc <## ("peer chat protocol version range: (" <> show minVer <> ", " <> show maxVer <> ")") diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 4343b547c9..949e745d2a 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -8,14 +8,19 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) +import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy.Char8 as LB import Simplex.Chat (roundedFDCount) import Simplex.Chat.Controller (ChatConfig (..), InlineFilesConfig (..), XFTPFileConfig (..), defaultInlineFilesConfig) +import Simplex.Chat.Mobile.File import Simplex.Chat.Options (ChatOpts (..)) import Simplex.FileTransfer.Client.Main (xftpClientCLI) import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..)) +import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) +import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util (unlessM) -import System.Directory (copyFile, doesFileExist) +import System.Directory (copyFile, createDirectoryIfMissing, doesFileExist, getFileSize) import System.Environment (withArgs) import System.IO.Silently (capture_) import Test.Hspec @@ -24,6 +29,7 @@ chatFileTests :: SpecWith FilePath chatFileTests = do describe "sending and receiving files" $ do describe "send and receive file" $ fileTestMatrix2 runTestFileTransfer + describe "send file, receive and locally encrypt file" $ fileTestMatrix2 runTestFileTransferEncrypted it "send and receive file inline (without accepting)" testInlineFileTransfer xit'' "accept inline file transfer, sender cancels during transfer" testAcceptInlineFileSndCancelDuringTransfer it "send and receive small file inline (default config)" testSmallInlineFileTransfer @@ -46,7 +52,7 @@ chatFileTests = do it "files folder: sender deleted file during transfer" testFilesFoldersImageSndDelete it "files folder: recipient deleted file during transfer" testFilesFoldersImageRcvDelete it "send and receive image with text and quote" testSendImageWithTextAndQuote - describe "send and receive image to group" testGroupSendImage + it "send and receive image to group" testGroupSendImage it "send and receive image with text and quote to group" testGroupSendImageWithTextAndQuote describe "async sending and receiving files" $ do -- fails on CI @@ -59,6 +65,7 @@ chatFileTests = do describe "file transfer over XFTP" $ do it "round file description count" $ const testXFTPRoundFDCount it "send and receive file" testXFTPFileTransfer + it "send and receive locally encrypted files" testXFTPFileTransferEncrypted it "send and receive file, accepting after upload" testXFTPAcceptAfterUpload it "send and receive file in group" testXFTPGroupFileTransfer it "delete uploaded file" testXFTPDeleteUploadedFile @@ -89,6 +96,37 @@ runTestFileTransfer alice bob = do dest <- B.readFile "./tests/tmp/test.pdf" dest `shouldBe` src +runTestFileTransferEncrypted :: HasCallStack => TestCC -> TestCC -> IO () +runTestFileTransferEncrypted alice bob = do + connectUsers alice bob + alice #> "/f @bob ./tests/fixtures/test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 encrypt=on ./tests/tmp" + bob <## "saving file 1 from alice to ./tests/tmp/test.pdf" + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob + concurrently_ + (bob <## "started receiving file 1 (test.pdf) from alice") + (alice <## "started sending file 1 (test.pdf) to bob") + + concurrentlyN_ + [ do + bob #> "@alice receiving here..." + -- uncomment this and below to test encryption error in encryptFile + -- bob <## "cannot write file ./tests/tmp/test.pdf: test error, received file not encrypted" + bob <## "completed receiving file 1 (test.pdf) from alice", + alice + <### [ WithTime "bob> receiving here...", + "completed sending file 1 (test.pdf) to bob" + ] + ] + src <- B.readFile "./tests/fixtures/test.pdf" + -- dest <- B.readFile "./tests/tmp/test.pdf" + -- dest `shouldBe` src + Right dest <- chatReadFile "./tests/tmp/test.pdf" (strEncode key) (strEncode nonce) + LB.toStrict dest `shouldBe` src + testInlineFileTransfer :: HasCallStack => FilePath -> IO () testInlineFileTransfer = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do @@ -724,11 +762,10 @@ testSendImageWithTextAndQuote = (alice <## "completed sending file 3 (test.jpg) to bob") B.readFile "./tests/tmp/test_1.jpg" `shouldReturn` src -testGroupSendImage :: SpecWith FilePath -testGroupSendImage = versionTestMatrix3 runTestGroupSendImage - where - runTestGroupSendImage :: HasCallStack => TestCC -> TestCC -> TestCC -> IO () - runTestGroupSendImage alice bob cath = do +testGroupSendImage :: HasCallStack => FilePath -> IO () +testGroupSendImage = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do createGroup3 "team" alice bob cath threadDelay 1000000 alice ##> "/_send #1 json {\"filePath\": \"./tests/fixtures/test.jpg\", \"msgContent\": {\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}" @@ -1013,6 +1050,35 @@ testXFTPFileTransfer = where cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} +testXFTPFileTransferEncrypted :: HasCallStack => FilePath -> IO () +testXFTPFileTransferEncrypted = + testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do + src <- B.readFile "./tests/fixtures/test.pdf" + srcLen <- getFileSize "./tests/fixtures/test.pdf" + let srcPath = "./tests/tmp/alice/test.pdf" + createDirectoryIfMissing True "./tests/tmp/alice/" + createDirectoryIfMissing True "./tests/tmp/bob/" + WFResult cfArgs <- chatWriteFile srcPath src + let fileJSON = LB.unpack $ J.encode $ CryptoFile srcPath $ Just cfArgs + withXFTPServer $ do + connectUsers alice bob + alice ##> ("/_send @2 json {\"msgContent\":{\"type\":\"file\", \"text\":\"\"}, \"fileSource\": " <> fileJSON <> "}") + alice <# "/f @bob ./tests/tmp/alice/test.pdf" + alice <## "use /fc 1 to cancel sending" + bob <# "alice> sends file test.pdf (266.0 KiB / 272376 bytes)" + bob <## "use /fr 1 [/ | ] to receive it" + bob ##> "/fr 1 encrypt=on ./tests/tmp/bob/" + bob <## "saving file 1 from alice to ./tests/tmp/bob/test.pdf" + Just (CFArgs key nonce) <- J.decode . LB.pack <$> getTermLine bob + alice <## "completed uploading file 1 (test.pdf) for bob" + bob <## "started receiving file 1 (test.pdf) from alice" + bob <## "completed receiving file 1 (test.pdf) from alice" + Right dest <- chatReadFile "./tests/tmp/bob/test.pdf" (strEncode key) (strEncode nonce) + LB.length dest `shouldBe` fromIntegral srcLen + LB.toStrict dest `shouldBe` src + where + cfg = testCfg {xftpFileConfig = Just $ XFTPFileConfig {minFileSize = 0}, tempDir = Just "./tests/tmp"} + testXFTPAcceptAfterUpload :: HasCallStack => FilePath -> IO () testXFTPAcceptAfterUpload = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do @@ -1334,6 +1400,7 @@ testXFTPRcvError tmp = do "started receiving file 1 (test.pdf) from alice" ] bob <## "error receiving file 1 (test.pdf) from alice" + _ <- getTermLine bob bob ##> "/fs 1" bob <## "receiving file 1 (test.pdf) error" @@ -1447,7 +1514,7 @@ startFileTransfer alice bob = startFileTransfer' alice bob "test.jpg" "136.5 KiB / 139737 bytes" startFileTransfer' :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () -startFileTransfer' cc1 cc2 fileName fileSize = startFileTransferWithDest' cc1 cc2 fileName fileSize $ Just "./tests/tmp" +startFileTransfer' cc1 cc2 fName fSize = startFileTransferWithDest' cc1 cc2 fName fSize $ Just "./tests/tmp" checkPartialTransfer :: HasCallStack => String -> IO () checkPartialTransfer fileName = do diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index bb556062cf..bf740a960f 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -10,8 +10,10 @@ import Control.Concurrent.Async (concurrently_) import Control.Monad (when) import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (GroupMemberRole (..)) +import Simplex.Messaging.Version import System.Directory (copyFile) import System.FilePath (()) import Test.Hspec @@ -19,7 +21,7 @@ import Test.Hspec chatGroupTests :: SpecWith FilePath chatGroupTests = do describe "chat groups" $ do - describe "add contacts, create group and send/receive messages" testGroup + it "add contacts, create group and send/receive messages" testGroup it "add contacts, create group and send/receive messages, check messages" testGroupCheckMessages it "create and join group with 4 members" testGroup2 it "create and delete group" testGroupDelete @@ -59,17 +61,49 @@ chatGroupTests = do it "show message decryption error" testGroupMsgDecryptError it "should report ratchet de-synchronization, synchronize ratchets" testGroupSyncRatchet it "synchronize ratchets, reset connection code" testGroupSyncRatchetCodeReset - describe "message reactions" $ do + describe "group message reactions" $ do it "set group message reactions" testSetGroupMessageReactions - -testGroup :: HasCallStack => SpecWith FilePath -testGroup = versionTestMatrix3 runTestGroup + describe "group delivery receipts" $ do + it "should send delivery receipts in group" testSendGroupDeliveryReceipts + it "should send delivery receipts in group depending on configuration" testConfigureGroupDeliveryReceipts + describe "direct connections in group are not established based on chat protocol version" $ do + describe "3 members group" $ do + testNoDirect _0 _0 True + testNoDirect _0 _1 True + testNoDirect _1 _0 False + testNoDirect _1 _1 False + it "members have different local display names in different groups" testNoDirectDifferentLDNs + it "member should connect to contact when profile match" testConnectMemberToContact + describe "create member contact" $ do + it "create contact with group member with invitation message" testMemberContactMessage + it "create contact with group member without invitation message" testMemberContactNoMessage + it "prohibited to create contact with group member if it already exists" testMemberContactProhibitedContactExists + it "prohibited to repeat sending x.grp.direct.inv" testMemberContactProhibitedRepeatInv + it "invited member replaces member contact reference if it already exists" testMemberContactInvitedConnectionReplaced + it "share incognito profile" testMemberContactIncognito + it "sends and updates profile when creating contact" testMemberContactProfileUpdate where - runTestGroup alice bob cath = testGroupShared alice bob cath False + _0 = supportedChatVRange -- don't create direct connections + _1 = groupCreateDirectVRange + -- having host configured with older version doesn't have effect in tests + -- because host uses current code and sends version in MemberInfo + testNoDirect vrMem2 vrMem3 noConns = + it + ( "host " <> vRangeStr supportedChatVRange + <> (", 2nd mem " <> vRangeStr vrMem2) + <> (", 3rd mem " <> vRangeStr vrMem3) + <> (if noConns then " : 2 3" else " : 2 <##> 3") + ) + $ testNoGroupDirectConns supportedChatVRange vrMem2 vrMem3 noConns + +testGroup :: HasCallStack => FilePath -> IO () +testGroup = + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ + \alice bob cath -> testGroupShared alice bob cath False testGroupCheckMessages :: HasCallStack => FilePath -> IO () testGroupCheckMessages = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> testGroupShared alice bob cath True testGroupShared :: HasCallStack => TestCC -> TestCC -> TestCC -> Bool -> IO () @@ -79,7 +113,7 @@ testGroupShared alice bob cath checkMessages = do alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" - alice ##> "/a team bob" + alice ##> "/a team bob admin" concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do @@ -91,7 +125,7 @@ testGroupShared alice bob cath checkMessages = do (alice <## "#team: bob joined the group") (bob <## "#team: you joined the group") when checkMessages $ threadDelay 1000000 -- for deterministic order of messages and "connected" events - alice ##> "/a team cath" + alice ##> "/a team cath admin" concurrentlyN_ [ alice <## "invitation to join the group #team sent to cath", do @@ -129,7 +163,7 @@ testGroupShared alice bob cath checkMessages = do when checkMessages $ getReadChats msgItem1 msgItem2 -- list groups alice ##> "/gs" - alice <## "#team" + alice <## "#team (3 members)" -- list group members alice ##> "/ms team" alice @@ -186,8 +220,12 @@ testGroupShared alice bob cath checkMessages = do -- delete contact alice ##> "/d bob" alice <## "bob: contact is deleted" - alice ##> "@bob hey" - alice <## "no contact bob" + alice `send` "@bob hey" + alice + <### [ "@bob hey", + "member #team bob does not have direct connection, creating", + "peer chat protocol version range incompatible" + ] when checkMessages $ threadDelay 1000000 alice #> "#team checking connection" bob <# "#team alice> checking connection" @@ -198,6 +236,7 @@ testGroupShared alice bob cath checkMessages = do alice @@@ [("@cath", "sent invitation to join group team as admin"), ("#team", "received")] bob @@@ [("@alice", "received invitation to join group team as admin"), ("@cath", "hey"), ("#team", "received")] -- test clearing chat + threadDelay 1000000 alice #$> ("/clear #team", id, "#team: all messages are removed locally ONLY") alice #$> ("/_get chat #1 count=100", chat, []) bob #$> ("/clear #team", id, "#team: all messages are removed locally ONLY") @@ -229,7 +268,7 @@ testGroupShared alice bob cath checkMessages = do testGroup2 :: HasCallStack => FilePath -> IO () testGroup2 = - testChat4 aliceProfile bobProfile cathProfile danProfile $ + testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do connectUsers alice bob connectUsers alice cath @@ -238,14 +277,14 @@ testGroup2 = alice ##> "/g club" alice <## "group #club is created" alice <## "to add members use /a club or /create link #club" - alice ##> "/a club bob" + alice ##> "/a club bob admin" concurrentlyN_ [ alice <## "invitation to join the group #club sent to bob", do bob <## "#club: alice invites you to join the group as admin" bob <## "use /j club to accept" ] - alice ##> "/a club cath" + alice ##> "/a club cath admin" concurrentlyN_ [ alice <## "invitation to join the group #club sent to cath", do @@ -270,7 +309,7 @@ testGroup2 = concurrentlyN_ [ bob <## "invitation to join the group #club sent to dan", do - dan <## "#club: bob invites you to join the group as admin" + dan <## "#club: bob invites you to join the group as member" dan <## "use /j club to accept" ] dan ##> "/j club" @@ -482,7 +521,7 @@ testGroupDeleteWhenInvited = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] bob ##> "/d #team" @@ -493,7 +532,7 @@ testGroupDeleteWhenInvited = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] @@ -509,7 +548,7 @@ testGroupReAddInvited = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] -- alice re-adds bob, he sees it as the same group @@ -517,7 +556,7 @@ testGroupReAddInvited = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] -- if alice removes bob and then re-adds him, she uses a new connection request @@ -528,7 +567,7 @@ testGroupReAddInvited = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team_1: alice invites you to join the group as admin" + bob <## "#team_1: alice invites you to join the group as member" bob <## "use /j team_1 to accept" ] @@ -544,7 +583,7 @@ testGroupReAddInvitedChangeRole = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] -- alice re-adds bob, he sees it as the same group @@ -584,7 +623,7 @@ testGroupDeleteInvitedContact = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] threadDelay 500000 @@ -598,11 +637,22 @@ testGroupDeleteInvitedContact = bob <# "#team alice> hello" bob #> "#team hi there" alice <# "#team bob> hi there" - alice ##> "@bob hey" - alice <## "no contact bob" - bob #> "@alice hey" - bob <## "[alice, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" - (alice hey", + "alice: security code changed" + ] + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + alice <##> bob testDeleteGroupMemberProfileKept :: HasCallStack => FilePath -> IO () testDeleteGroupMemberProfileKept = @@ -617,7 +667,7 @@ testDeleteGroupMemberProfileKept = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## "use /j team to accept" ] bob ##> "/j team" @@ -636,7 +686,7 @@ testDeleteGroupMemberProfileKept = concurrentlyN_ [ alice <## "invitation to join the group #club sent to bob", do - bob <## "#club: alice invites you to join the group as admin" + bob <## "#club: alice invites you to join the group as member" bob <## "use /j club to accept" ] bob ##> "/j club" @@ -651,7 +701,7 @@ testDeleteGroupMemberProfileKept = alice ##> "/d bob" alice <## "bob: contact is deleted" alice ##> "@bob hey" - alice <## "no contact bob" + alice <## "no contact bob, use @#club bob " bob #> "@alice hey" bob <## "[alice, contactId: 2, connId: 1] error: connection authorization failed - this could happen if connection was deleted, secured with different credentials, or due to a bug - please re-create the connection" (alice FilePath -> IO () testGroupRemoveAdd = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath -- remove member @@ -689,7 +739,7 @@ testGroupRemoveAdd = ] alice ##> "/a team bob" alice <## "invitation to join the group #team sent to bob" - bob <## "#team_1: alice invites you to join the group as admin" + bob <## "#team_1: alice invites you to join the group as member" bob <## "use /j team_1 to accept" bob ##> "/j team_1" concurrentlyN_ @@ -730,27 +780,27 @@ testGroupList = concurrentlyN_ [ alice <## "invitation to join the group #tennis sent to bob", do - bob <## "#tennis: alice invites you to join the group as admin" + bob <## "#tennis: alice invites you to join the group as member" bob <## "use /j tennis to accept" ] -- alice sees both groups alice ##> "/gs" - alice <### ["#team", "#tennis"] + alice <### ["#team (2 members)", "#tennis (1 member)"] -- bob sees #tennis as invitation bob ##> "/gs" bob - <### [ "#team", + <### [ "#team (2 members)", "#tennis - you are invited (/j tennis to join, /d #tennis to delete invitation)" ] -- after deleting invitation bob sees only one group bob ##> "/d #tennis" bob <## "#tennis: you deleted the group" bob ##> "/gs" - bob <## "#team" + bob <## "#team (2 members)" testGroupMessageQuotedReply :: HasCallStack => FilePath -> IO () testGroupMessageQuotedReply = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath threadDelay 1000000 @@ -976,6 +1026,7 @@ testGroupMessageDelete = (bob <# "#team alice> hello!") (cath <# "#team alice> hello!") + threadDelay 1000000 msgItemId1 <- lastItemId alice alice #$> ("/_delete item #1 " <> msgItemId1 <> " internal", id, "message deleted") @@ -1172,7 +1223,7 @@ testGroupDeleteUnusedContacts = concurrentlyN_ [ alice <## "invitation to join the group #club sent to bob", do - bob <## "#club: alice invites you to join the group as admin" + bob <## "#club: alice invites you to join the group as member" bob <## "use /j club to accept" ] bob ##> "/j club" @@ -1183,7 +1234,7 @@ testGroupDeleteUnusedContacts = concurrentlyN_ [ alice <## "invitation to join the group #club sent to cath", do - cath <## "#club: alice invites you to join the group as admin" + cath <## "#club: alice invites you to join the group as member" cath <## "use /j club to accept" ] cath ##> "/j club" @@ -1227,7 +1278,7 @@ testGroupDeleteUnusedContacts = cath <## "alice (Alice)" cath `hasContactProfiles` ["alice", "cath"] where - cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0} + cfg = mkCfgCreateGroupDirect $ testCfg {initialCleanupManagerDelay = 0, cleanupManagerInterval = 1, cleanupManagerStepDelay = 0} deleteGroup :: HasCallStack => TestCC -> TestCC -> TestCC -> String -> IO () deleteGroup alice bob cath group = do alice ##> ("/d #" <> group) @@ -1316,7 +1367,7 @@ testGroupDescription = testChat4 aliceProfile bobProfile cathProfile danProfile testGroupModerate :: HasCallStack => FilePath -> IO () testGroupModerate = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath alice ##> "/mr team cath member" @@ -1347,7 +1398,7 @@ testGroupModerate = testGroupModerateFullDelete :: HasCallStack => FilePath -> IO () testGroupModerateFullDelete = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath alice ##> "/mr team cath member" @@ -1385,10 +1436,10 @@ testGroupModerateFullDelete = testGroupDelayedModeration :: HasCallStack => FilePath -> IO () testGroupDelayedModeration tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob - withNewTestChat tmp "cath" cathProfile $ \cath -> do + withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do connectUsers alice cath addMember "team" alice cath GRMember cath ##> "/j team" @@ -1402,11 +1453,11 @@ testGroupDelayedModeration tmp = do alice ##> "\\\\ #team @cath hi" alice <## "message marked deleted by you" cath <# "#team cath> [marked deleted by alice] hi" - withTestChat tmp "bob" $ \bob -> do + withTestChatCfg tmp cfg "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)" - withTestChat tmp "cath" $ \cath -> do + withTestChatCfg tmp cfg "cath" $ \cath -> do cath <## "2 contacts connected (use /cs for the list)" cath <## "#team: connected to server(s)" cath <## "#team: member bob (Bob) is connected" @@ -1419,13 +1470,15 @@ testGroupDelayedModeration tmp = do bob ##> "/_get chat #1 count=2" r <- chat <$> getTermLine bob r `shouldMatchList` [(0, "connected"), (0, "hi [marked deleted by alice]")] + where + cfg = testCfgCreateGroupDirect testGroupDelayedModerationFullDelete :: HasCallStack => FilePath -> IO () testGroupDelayedModerationFullDelete tmp = do - withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do createGroup2 "team" alice bob - withNewTestChat tmp "cath" cathProfile $ \cath -> do + withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do connectUsers alice cath addMember "team" alice cath GRMember cath ##> "/j team" @@ -1447,14 +1500,14 @@ testGroupDelayedModerationFullDelete tmp = do cath <## "alice updated group #team:" cath <## "updated group preferences:" cath <## "Full deletion: on" - withTestChat tmp "bob" $ \bob -> do + withTestChatCfg tmp cfg "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob <## "#team: alice added cath (Catherine) to the group (connecting...)" bob <## "alice updated group #team:" bob <## "updated group preferences:" bob <## "Full deletion: on" - withTestChat tmp "cath" $ \cath -> do + withTestChatCfg tmp cfg "cath" $ \cath -> do cath <## "2 contacts connected (use /cs for the list)" cath <## "#team: connected to server(s)" cath <## "#team: member bob (Bob) is connected" @@ -1467,6 +1520,8 @@ testGroupDelayedModerationFullDelete tmp = do bob ##> "/_get chat #1 count=3" r <- chat <$> getTermLine bob r `shouldMatchList` [(0, "Full deletion: on"), (0, "connected"), (0, "moderated [deleted by alice]")] + where + cfg = testCfgCreateGroupDirect testGroupAsync :: HasCallStack => FilePath -> IO () testGroupAsync tmp = do @@ -1812,8 +1867,7 @@ testGroupLinkIncognitoMembership = -- bob connected incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) + bob ##> ("/c i " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob concurrentlyN_ @@ -1822,13 +1876,12 @@ testGroupLinkIncognitoMembership = bob <## "use /i alice to print out this incognito profile again", alice <## (bobIncognito <> ": contact is connected") ] - bob #$> ("/incognito off", id, "ok") -- alice creates group alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" -- alice invites bob - alice ##> ("/a team " <> bobIncognito) + alice ##> ("/a team " <> bobIncognito <> " admin") concurrentlyN_ [ alice <## ("invitation to join the group #team sent to " <> bobIncognito), do @@ -1865,8 +1918,7 @@ testGroupLinkIncognitoMembership = cath #> ("@" <> bobIncognito <> " hey, I'm cath") bob ?<# "cath> hey, I'm cath" -- dan joins incognito - dan #$> ("/incognito on", id, "ok") - dan ##> ("/c " <> gLink) + dan ##> ("/c i " <> gLink) danIncognito <- getTermLine dan dan <## "connection request sent incognito!" bob <## (danIncognito <> ": accepting request to join group #team...") @@ -1893,7 +1945,6 @@ testGroupLinkIncognitoMembership = cath <## ("#team: " <> bobIncognito <> " added " <> danIncognito <> " to the group (connecting...)") cath <## ("#team: new member " <> danIncognito <> " is connected") ] - dan #$> ("/incognito off", id, "ok") bob ?#> ("@" <> danIncognito <> " hi, I'm incognito") dan ?<# (bobIncognito <> "> hi, I'm incognito") dan ?#> ("@" <> bobIncognito <> " hey, me too") @@ -2001,7 +2052,6 @@ testGroupLinkIncognitoUnusedHostContactsDeleted :: HasCallStack => FilePath -> I testGroupLinkIncognitoUnusedHostContactsDeleted = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do - bob #$> ("/incognito on", id, "ok") bobIncognitoTeam <- createGroupBobIncognito alice bob "team" "alice" bobIncognitoClub <- createGroupBobIncognito alice bob "club" "alice_1" bobIncognitoTeam `shouldNotBe` bobIncognitoClub @@ -2031,7 +2081,7 @@ testGroupLinkIncognitoUnusedHostContactsDeleted = alice <## ("to add members use /a " <> group <> " or /create link #" <> group) alice ##> ("/create link #" <> group) gLinkTeam <- getGroupLink alice group GRMember True - bob ##> ("/c " <> gLinkTeam) + bob ##> ("/c i " <> gLinkTeam) bobIncognito <- getTermLine bob bob <## "connection request sent incognito!" alice <## (bobIncognito <> ": accepting request to join group #" <> group <> "...") @@ -2127,7 +2177,7 @@ testGroupLinkMemberRole = testGroupLinkLeaveDelete :: HasCallStack => FilePath -> IO () testGroupLinkLeaveDelete = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do connectUsers alice bob connectUsers cath bob @@ -2197,47 +2247,46 @@ testGroupLinkLeaveDelete = testGroupMsgDecryptError :: HasCallStack => FilePath -> IO () testGroupMsgDecryptError tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "cath" cathProfile $ \cath -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - createGroup3 "team" alice bob cath - alice #> "#team hi" - [bob, cath] *<# "#team alice> hi" - bob #> "#team hey" - [alice, cath] *<# "#team bob> hey" - setupDesynchronizedRatchet tmp alice cath - withTestChat tmp "bob" $ \bob -> do - bob <## "2 contacts connected (use /cs for the list)" - bob <## "#team: connected to server(s)" - alice #> "#team hello again" - bob <# "#team alice> skipped message ID 8..10" - [bob, cath] *<# "#team alice> hello again" - bob #> "#team received!" - alice <# "#team bob> received!" - cath <# "#team bob> received!" + withNewTestChat tmp "bob" bobProfile $ \bob -> do + createGroup2 "team" alice bob + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + setupDesynchronizedRatchet tmp alice + withTestChat tmp "bob" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)" + alice #> "#team hello again" + bob <# "#team alice> skipped message ID 10..12" + bob <# "#team alice> hello again" + bob #> "#team received!" + alice <# "#team bob> received!" -setupDesynchronizedRatchet :: HasCallStack => FilePath -> TestCC -> TestCC -> IO () -setupDesynchronizedRatchet tmp alice cath = do +setupDesynchronizedRatchet :: HasCallStack => FilePath -> TestCC -> IO () +setupDesynchronizedRatchet tmp alice = do copyDb "bob" "bob_old" withTestChat tmp "bob" $ \bob -> do - bob <## "2 contacts connected (use /cs for the list)" + bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" - alice #> "#team hello" - [bob, cath] *<# "#team alice> hello" - bob #> "#team hello too" - [alice, cath] *<# "#team bob> hello too" + alice #> "#team 1" + bob <# "#team alice> 1" + bob #> "#team 2" + alice <# "#team bob> 2" + alice #> "#team 3" + bob <# "#team alice> 3" + bob #> "#team 4" + alice <# "#team bob> 4" withTestChat tmp "bob_old" $ \bob -> do - bob <## "2 contacts connected (use /cs for the list)" + bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob ##> "/sync #team alice" bob <## "error: command is prohibited" alice #> "#team 1" bob <## "#team alice: decryption error (connection out of sync), synchronization required" bob <## "use /sync #team alice to synchronize" - cath <# "#team alice> 1" alice #> "#team 2" - cath <# "#team alice> 2" alice #> "#team 3" - cath <# "#team alice> 3" (bob "/tail #team 1" bob <# "#team alice> decryption error, possibly due to the device change (header, 3 messages)" @@ -2249,106 +2298,92 @@ setupDesynchronizedRatchet tmp alice cath = do testGroupSyncRatchet :: HasCallStack => FilePath -> IO () testGroupSyncRatchet tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "cath" cathProfile $ \cath -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - createGroup3 "team" alice bob cath - alice #> "#team hi" - [bob, cath] *<# "#team alice> hi" - bob #> "#team hey" - [alice, cath] *<# "#team bob> hey" - setupDesynchronizedRatchet tmp alice cath - withTestChat tmp "bob_old" $ \bob -> do - bob <## "2 contacts connected (use /cs for the list)" - bob <## "#team: connected to server(s)" - -- cath and bob are not fully de-synchronized - bob `send` "#team 1" - bob <## "error: command is prohibited" -- silence? - bob <# "#team 1" - (alice "#team 1" - [alice, bob] *<# "#team cath> 1" - bob `send` "#team 2" - bob <## "error: command is prohibited" - bob <# "#team 2" - cath <# "#team bob> incorrect message hash" - cath <# "#team bob> 2" - bob `send` "#team 3" - bob <## "error: command is prohibited" - bob <# "#team 3" - cath <# "#team bob> 3" - -- synchronize bob and alice - bob ##> "/sync #team alice" - bob <## "connection synchronization started" - alice <## "#team bob: connection synchronization agreed" - bob <## "#team alice: connection synchronization agreed" - alice <## "#team bob: connection synchronized" - bob <## "#team alice: connection synchronized" + withNewTestChat tmp "bob" bobProfile $ \bob -> do + createGroup2 "team" alice bob + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + setupDesynchronizedRatchet tmp alice + withTestChat tmp "bob_old" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)" + bob `send` "#team 1" + bob <## "error: command is prohibited" -- silence? + bob <# "#team 1" + (alice "/sync #team alice" + bob <## "connection synchronization started" + alice <## "#team bob: connection synchronization agreed" + bob <## "#team alice: connection synchronization agreed" + alice <## "#team bob: connection synchronized" + bob <## "#team alice: connection synchronized" - bob #$> ("/_get chat #1 count=3", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "connection synchronized")]) - alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) + bob #$> ("/_get chat #1 count=3", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "connection synchronized")]) + alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) - alice #> "#team hello again" - [bob, cath] *<# "#team alice> hello again" - bob #> "#team received!" - alice <# "#team bob> received!" - cath <# "#team bob> received!" + alice #> "#team hello again" + bob <# "#team alice> hello again" + bob #> "#team received!" + alice <# "#team bob> received!" testGroupSyncRatchetCodeReset :: HasCallStack => FilePath -> IO () testGroupSyncRatchetCodeReset tmp = withNewTestChat tmp "alice" aliceProfile $ \alice -> do - withNewTestChat tmp "cath" cathProfile $ \cath -> do - withNewTestChat tmp "bob" bobProfile $ \bob -> do - createGroup3 "team" alice bob cath - alice #> "#team hi" - [bob, cath] *<# "#team alice> hi" - bob #> "#team hey" - [alice, cath] *<# "#team bob> hey" - -- connection not verified - bob ##> "/i #team alice" - aliceInfo bob - bob <## "connection not verified, use /code command to see security code" - -- verify connection - alice ##> "/code #team bob" - bCode <- getTermLine alice - bob ##> ("/verify #team alice " <> bCode) - bob <## "connection verified" - -- connection verified - bob ##> "/i #team alice" - aliceInfo bob - bob <## "connection verified" - setupDesynchronizedRatchet tmp alice cath - withTestChat tmp "bob_old" $ \bob -> do - bob <## "2 contacts connected (use /cs for the list)" - bob <## "#team: connected to server(s)" - bob ##> "/sync #team alice" - bob <## "connection synchronization started" - alice <## "#team bob: connection synchronization agreed" - bob <## "#team alice: connection synchronization agreed" - bob <## "#team alice: security code changed" - alice <## "#team bob: connection synchronized" - bob <## "#team alice: connection synchronized" + withNewTestChat tmp "bob" bobProfile $ \bob -> do + createGroup2 "team" alice bob + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + -- connection not verified + bob ##> "/i #team alice" + aliceInfo bob False + -- verify connection + alice ##> "/code #team bob" + bCode <- getTermLine alice + bob ##> ("/verify #team alice " <> bCode) + bob <## "connection verified" + -- connection verified + bob ##> "/i #team alice" + aliceInfo bob True + setupDesynchronizedRatchet tmp alice + withTestChat tmp "bob_old" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)" + bob ##> "/sync #team alice" + bob <## "connection synchronization started" + alice <## "#team bob: connection synchronization agreed" + bob <## "#team alice: connection synchronization agreed" + bob <## "#team alice: security code changed" + alice <## "#team bob: connection synchronized" + bob <## "#team alice: connection synchronized" - bob #$> ("/_get chat #1 count=4", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")]) - alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) + bob #$> ("/_get chat #1 count=4", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")]) + alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) - -- connection not verified - bob ##> "/i #team alice" - aliceInfo bob - bob <## "connection not verified, use /code command to see security code" + -- connection not verified + bob ##> "/i #team alice" + aliceInfo bob False - alice #> "#team hello again" - [bob, cath] *<# "#team alice> hello again" - bob #> "#team received!" - alice <# "#team bob> received!" - (cath "#team hello again" + bob <# "#team alice> hello again" + bob #> "#team received!" + alice <# "#team bob> received!" where - aliceInfo :: HasCallStack => TestCC -> IO () - aliceInfo bob = do + aliceInfo :: HasCallStack => TestCC -> Bool -> IO () + aliceInfo bob verified = do bob <## "group ID: 1" bob <## "member ID: 1" bob <## "receiving messages via: localhost" bob <## "sending messages via: localhost" + bob <## connVerified + bob <## currentChatVRangeInfo + where + connVerified + | verified = "connection verified" + | otherwise = "connection not verified, use /code command to see security code" testSetGroupMessageReactions :: HasCallStack => FilePath -> IO () testSetGroupMessageReactions = @@ -2418,3 +2453,670 @@ testSetGroupMessageReactions = cath ##> "/tail #team 1" cath <# "#team alice> hi" cath <## " 👍 1" + +testSendGroupDeliveryReceipts :: HasCallStack => FilePath -> IO () +testSendGroupDeliveryReceipts tmp = + withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do + -- turn off contacts receipts for tests + alice ##> "/_set receipts contacts 1 off" + alice <## "ok" + bob ##> "/_set receipts contacts 1 off" + bob <## "ok" + cath ##> "/_set receipts contacts 1 off" + cath <## "ok" + + createGroup3 "team" alice bob cath + threadDelay 1000000 + + alice #> "#team hi" + bob <# "#team alice> hi" + cath <# "#team alice> hi" + alice % "#team hi" + alice ⩗ "#team hi" + + bob #> "#team hey" + alice <# "#team bob> hey" + cath <# "#team bob> hey" + bob % "#team hey" + bob ⩗ "#team hey" + where + cfg = testCfg {showReceipts = True} + +testConfigureGroupDeliveryReceipts :: HasCallStack => FilePath -> IO () +testConfigureGroupDeliveryReceipts tmp = + withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp cfg "cath" cathProfile $ \cath -> do + -- turn off contacts receipts for tests + alice ##> "/_set receipts contacts 1 off" + alice <## "ok" + bob ##> "/_set receipts contacts 1 off" + bob <## "ok" + cath ##> "/_set receipts contacts 1 off" + cath <## "ok" + + -- create group 1 + createGroup3 "team" alice bob cath + threadDelay 1000000 + + -- create group 2 + alice ##> "/g club" + alice <## "group #club is created" + alice <## "to add members use /a club or /create link #club" + alice ##> "/a club bob" + concurrentlyN_ + [ alice <## "invitation to join the group #club sent to bob", + do + bob <## "#club: alice invites you to join the group as member" + bob <## "use /j club to accept" + ] + bob ##> "/j club" + concurrently_ + (alice <## "#club: bob joined the group") + (bob <## "#club: you joined the group") + alice ##> "/a club cath" + concurrentlyN_ + [ alice <## "invitation to join the group #club sent to cath", + do + cath <## "#club: alice invites you to join the group as member" + cath <## "use /j club to accept" + ] + cath ##> "/j club" + concurrentlyN_ + [ alice <## "#club: cath joined the group", + do + cath <## "#club: you joined the group" + cath <## "#club: member bob_1 (Bob) is connected" + cath <## "contact bob_1 is merged into bob" + cath <## "use @bob to send messages", + do + bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)" + bob <## "#club: new member cath_1 is connected" + bob <## "contact cath_1 is merged into cath" + bob <## "use @cath to send messages" + ] + threadDelay 1000000 + + -- for new users receipts are enabled by default + receipt bob alice cath "team" "1" + receipt bob alice cath "club" "2" + + -- configure receipts in all chats + alice ##> "/set receipts all off" + alice <## "ok" + partialReceipt bob alice cath "team" "3" + partialReceipt bob alice cath "club" "4" + + -- configure receipts for user groups + alice ##> "/_set receipts groups 1 on" + alice <## "ok" + receipt bob alice cath "team" "5" + receipt bob alice cath "club" "6" + + -- configure receipts for user groups (terminal api) + alice ##> "/set receipts groups off" + alice <## "ok" + partialReceipt bob alice cath "team" "7" + partialReceipt bob alice cath "club" "8" + + -- configure receipts for group + alice ##> "/receipts #team on" + alice <## "ok" + receipt bob alice cath "team" "9" + partialReceipt bob alice cath "club" "10" + + -- configure receipts for user groups (don't clear overrides) + alice ##> "/_set receipts groups 1 off" + alice <## "ok" + receipt bob alice cath "team" "11" + partialReceipt bob alice cath "club" "12" + + alice ##> "/_set receipts groups 1 off clear_overrides=off" + alice <## "ok" + receipt bob alice cath "team" "13" + partialReceipt bob alice cath "club" "14" + + -- configure receipts for user groups (clear overrides) + alice ##> "/set receipts groups off clear_overrides=on" + alice <## "ok" + partialReceipt bob alice cath "team" "15" + partialReceipt bob alice cath "club" "16" + + -- configure receipts for group, reset to default + alice ##> "/receipts #team on" + alice <## "ok" + receipt bob alice cath "team" "17" + partialReceipt bob alice cath "club" "18" + + alice ##> "/receipts #team default" + alice <## "ok" + partialReceipt bob alice cath "team" "19" + partialReceipt bob alice cath "club" "20" + + -- cath - disable receipts for user groups + cath ##> "/_set receipts groups 1 off" + cath <## "ok" + noReceipt bob alice cath "team" "21" + noReceipt bob alice cath "club" "22" + + -- partial, all receipts in one group; no receipts in other group + cath ##> "/receipts #team on" + cath <## "ok" + partialReceipt bob alice cath "team" "23" + noReceipt bob alice cath "club" "24" + + alice ##> "/receipts #team on" + alice <## "ok" + receipt bob alice cath "team" "25" + noReceipt bob alice cath "club" "26" + where + cfg = mkCfgCreateGroupDirect $ testCfg {showReceipts = True} + receipt cc1 cc2 cc3 gName msg = do + name1 <- userName cc1 + cc1 #> ("#" <> gName <> " " <> msg) + cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc1 % ("#" <> gName <> " " <> msg) + cc1 ⩗ ("#" <> gName <> " " <> msg) + partialReceipt cc1 cc2 cc3 gName msg = do + name1 <- userName cc1 + cc1 #> ("#" <> gName <> " " <> msg) + cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc1 % ("#" <> gName <> " " <> msg) + noReceipt cc1 cc2 cc3 gName msg = do + name1 <- userName cc1 + cc1 #> ("#" <> gName <> " " <> msg) + cc2 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc3 <# ("#" <> gName <> " " <> name1 <> "> " <> msg) + cc1 VersionRange -> VersionRange -> VersionRange -> Bool -> FilePath -> IO () +testNoGroupDirectConns hostVRange mem2VRange mem3VRange noDirectConns tmp = + withNewTestChatCfg tmp testCfg {chatVRange = hostVRange} "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp testCfg {chatVRange = mem2VRange} "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp testCfg {chatVRange = mem3VRange} "cath" cathProfile $ \cath -> do + createGroup3 "team" alice bob cath + if noDirectConns + then contactsDontExist bob cath + else contactsExist bob cath + where + contactsDontExist bob cath = do + bob ##> "/contacts" + bob <## "alice (Alice)" + cath ##> "/contacts" + cath <## "alice (Alice)" + contactsExist bob cath = do + bob ##> "/contacts" + bob + <### [ "alice (Alice)", + "cath (Catherine)" + ] + cath ##> "/contacts" + cath + <### [ "alice (Alice)", + "bob (Bob)" + ] + bob <##> cath + +testNoDirectDifferentLDNs :: HasCallStack => FilePath -> IO () +testNoDirectDifferentLDNs = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + alice ##> "/g club" + alice <## "group #club is created" + alice <## "to add members use /a club or /create link #club" + addMember "club" alice bob GRAdmin + bob ##> "/j club" + concurrently_ + (alice <## "#club: bob joined the group") + (bob <## "#club: you joined the group") + addMember "club" alice cath GRAdmin + cath ##> "/j club" + concurrentlyN_ + [ alice <## "#club: cath joined the group", + do + cath <## "#club: you joined the group" + cath <## "#club: member bob_1 (Bob) is connected", + do + bob <## "#club: alice added cath_1 (Catherine) to the group (connecting...)" + bob <## "#club: new member cath_1 is connected" + ] + + testGroupLDNs alice bob cath "team" "bob" "cath" + testGroupLDNs alice bob cath "club" "bob_1" "cath_1" + + alice `hasContactProfiles` ["alice", "bob", "cath"] + bob `hasContactProfiles` ["bob", "alice", "cath", "cath"] + cath `hasContactProfiles` ["cath", "alice", "bob", "bob"] + where + testGroupLDNs alice bob cath gName bobLDN cathLDN = do + alice ##> ("/ms " <> gName) + alice + <### [ "alice (Alice): owner, you, created group", + "bob (Bob): admin, invited, connected", + "cath (Catherine): admin, invited, connected" + ] + + bob ##> ("/ms " <> gName) + bob + <### [ "alice (Alice): owner, host, connected", + "bob (Bob): admin, you, connected", + ConsoleString (cathLDN <> " (Catherine): admin, connected") + ] + + cath ##> ("/ms " <> gName) + cath + <### [ "alice (Alice): owner, host, connected", + ConsoleString (bobLDN <> " (Bob): admin, connected"), + "cath (Catherine): admin, you, connected" + ] + + alice #> ("#" <> gName <> " hello") + concurrentlyN_ + [ bob <# ("#" <> gName <> " alice> hello"), + cath <# ("#" <> gName <> " alice> hello") + ] + bob #> ("#" <> gName <> " hi there") + concurrentlyN_ + [ alice <# ("#" <> gName <> " bob> hi there"), + cath <# ("#" <> gName <> " " <> bobLDN <> "> hi there") + ] + cath #> ("#" <> gName <> " hey") + concurrentlyN_ + [ alice <# ("#" <> gName <> " cath> hey"), + bob <# ("#" <> gName <> " " <> cathLDN <> "> hey") + ] + +testConnectMemberToContact :: HasCallStack => FilePath -> IO () +testConnectMemberToContact = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + connectUsers alice bob + connectUsers alice cath + createGroup2 "team" bob cath + bob ##> "/a #team alice" + bob <## "invitation to join the group #team sent to alice" + alice <## "#team: bob invites you to join the group as member" + alice <## "use /j team to accept" + alice ##> "/j team" + concurrentlyN_ + [ do + alice <## "#team: you joined the group" + alice <## "#team: member cath_1 (Catherine) is connected" + alice <## "member #team cath_1 is merged into cath", + do + bob <## "#team: alice joined the group", + do + cath <## "#team: bob added alice_1 (Alice) to the group (connecting...)" + cath <## "#team: new member alice_1 is connected" + cath <## "member #team alice_1 is merged into alice" + ] + alice <##> cath + alice #> "#team hello" + bob <# "#team alice> hello" + cath <# "#team alice> hello" + cath #> "#team hello too" + bob <# "#team cath> hello too" + alice <# "#team cath> hello too" + + alice ##> "/contacts" + alice + <### [ "bob (Bob)", + "cath (Catherine)" + ] + cath ##> "/contacts" + cath + <### [ "alice (Alice)", + "bob (Bob)" + ] + alice `hasContactProfiles` ["alice", "bob", "cath"] + cath `hasContactProfiles` ["cath", "alice", "bob"] + +testMemberContactMessage :: HasCallStack => FilePath -> IO () +testMemberContactMessage = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + -- alice and bob delete contacts, connect + alice ##> "/d bob" + alice <## "bob: contact is deleted" + bob ##> "/d alice" + bob <## "alice: contact is deleted" + + alice ##> "@#team bob hi" + alice + <### [ "member #team bob does not have direct connection, creating", + "contact for member #team bob is created", + "sent invitation to connect directly to member #team bob", + WithTime "@bob hi" + ] + bob + <### [ "#team alice is creating direct contact alice with you", + WithTime "alice> hi" + ] + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + + bob #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) + alice <##> bob + + -- bob and cath connect + bob ##> "@#team cath hi" + bob + <### [ "member #team cath does not have direct connection, creating", + "contact for member #team cath is created", + "sent invitation to connect directly to member #team cath", + WithTime "@cath hi" + ] + cath + <### [ "#team bob is creating direct contact bob with you", + WithTime "bob> hi" + ] + concurrently_ + (bob <## "cath (Catherine): contact is connected") + (cath <## "bob (Bob): contact is connected") + + cath #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) + bob <##> cath + +testMemberContactNoMessage :: HasCallStack => FilePath -> IO () +testMemberContactNoMessage = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + -- bob and cath connect + bob ##> "/_create member contact #1 3" + bob <## "contact for member #team cath is created" + + bob ##> "/_invite member contact @3" + bob <## "sent invitation to connect directly to member #team cath" + cath <## "#team bob is creating direct contact bob with you" + concurrently_ + (bob <## "cath (Catherine): contact is connected") + (cath <## "bob (Bob): contact is connected") + + cath #$> ("/_get chat #1 count=1", chat, [(0, "started direct connection with you")]) + bob <##> cath + +testMemberContactProhibitedContactExists :: HasCallStack => FilePath -> IO () +testMemberContactProhibitedContactExists = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + alice ##> "/_create member contact #1 2" + alice <## "bad chat command: member contact already exists" + + alice ##> "@#team bob hi" + alice <# "@bob hi" + bob <# "alice> hi" + +testMemberContactProhibitedRepeatInv :: HasCallStack => FilePath -> IO () +testMemberContactProhibitedRepeatInv = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + bob ##> "/_create member contact #1 3" + bob <## "contact for member #team cath is created" + + bob ##> "/_invite member contact @3 text hi" + bob + <### [ "sent invitation to connect directly to member #team cath", + WithTime "@cath hi" + ] + bob ##> "/_invite member contact @3 text hey" + bob <## "bad chat command: x.grp.direct.inv already sent" + cath + <### [ "#team bob is creating direct contact bob with you", + WithTime "bob> hi" + ] + concurrently_ + (bob <## "cath (Catherine): contact is connected") + (cath <## "bob (Bob): contact is connected") + + bob <##> cath + +testMemberContactInvitedConnectionReplaced :: HasCallStack => FilePath -> IO () +testMemberContactInvitedConnectionReplaced tmp = do + withNewTestChat tmp "alice" aliceProfile $ \alice -> do + withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChat tmp "cath" cathProfile $ \cath -> do + createGroup3 "team" alice bob cath + + alice ##> "/d bob" + alice <## "bob: contact is deleted" + + alice ##> "@#team bob hi" + alice + <### [ "member #team bob does not have direct connection, creating", + "contact for member #team bob is created", + "sent invitation to connect directly to member #team bob", + WithTime "@bob hi" + ] + bob + <### [ "#team alice is creating direct contact alice with you", + WithTime "alice> hi", + "alice: security code changed" + ] + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + + bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "received invitation to join group team as admin"), (0, "hi"), (0, "security code changed")] <> chatFeatures) + + withTestChat tmp "bob" $ \bob -> do + subscriptions bob 1 + + checkConnectionsWork alice bob + + withTestChat tmp "alice" $ \alice -> do + subscriptions alice 2 + + withTestChat tmp "bob" $ \bob -> do + subscriptions bob 1 + + checkConnectionsWork alice bob + + withTestChat tmp "cath" $ \cath -> do + subscriptions cath 1 + + -- group messages work + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + bob #> "#team hi there" + concurrently_ + (alice <# "#team bob> hi there") + (cath <# "#team bob> hi there") + cath #> "#team hey team" + concurrently_ + (alice <# "#team cath> hey team") + (bob <# "#team cath> hey team") + where + subscriptions :: TestCC -> Int -> IO () + subscriptions cc n = do + cc <## (show n <> " contacts connected (use /cs for the list)") + cc <## "#team: connected to server(s)" + checkConnectionsWork alice bob = do + alice <##> bob + alice @@@ [("@bob", "hey"), ("@cath", "sent invitation to join group team as admin"), ("#team", "connected")] + bob @@@ [("@alice", "hey"), ("#team", "started direct connection with you")] + +testMemberContactIncognito :: HasCallStack => FilePath -> IO () +testMemberContactIncognito = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + -- create group, bob joins incognito + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/create link #team" + gLink <- getGroupLink alice "team" GRMember True + bob ##> ("/c i " <> gLink) + bobIncognito <- getTermLine bob + bob <## "connection request sent incognito!" + alice <## (bobIncognito <> ": accepting request to join group #team...") + _ <- getTermLine bob + concurrentlyN_ + [ do + alice <## (bobIncognito <> ": contact is connected") + alice <## (bobIncognito <> " invited to group #team via your group link") + alice <## ("#team: " <> bobIncognito <> " joined the group"), + do + bob <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## "use /i alice to print out this incognito profile again" + bob <## ("#team: you joined the group incognito as " <> bobIncognito) + ] + -- cath joins incognito + cath ##> ("/c i " <> gLink) + cathIncognito <- getTermLine cath + cath <## "connection request sent incognito!" + alice <## (cathIncognito <> ": accepting request to join group #team...") + _ <- getTermLine cath + concurrentlyN_ + [ do + alice <## (cathIncognito <> ": contact is connected") + alice <## (cathIncognito <> " invited to group #team via your group link") + alice <## ("#team: " <> cathIncognito <> " joined the group"), + do + cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito) + cath <## "use /i alice to print out this incognito profile again" + cath <## ("#team: you joined the group incognito as " <> cathIncognito) + cath <## ("#team: member " <> bobIncognito <> " is connected"), + do + bob <## ("#team: alice added " <> cathIncognito <> " to the group (connecting...)") + bob <## ("#team: new member " <> cathIncognito <> " is connected") + ] + + alice `hasContactProfiles` ["alice", T.pack bobIncognito, T.pack cathIncognito] + bob `hasContactProfiles` ["bob", "alice", T.pack bobIncognito, T.pack cathIncognito] + cath `hasContactProfiles` ["cath", "alice", T.pack bobIncognito, T.pack cathIncognito] + + -- bob creates member contact with cath - both share incognito profile + bob ##> ("@#team " <> cathIncognito <> " hi") + bob + <### [ ConsoleString ("member #team " <> cathIncognito <> " does not have direct connection, creating"), + ConsoleString ("contact for member #team " <> cathIncognito <> " is created"), + ConsoleString ("sent invitation to connect directly to member #team " <> cathIncognito), + WithTime ("i @" <> cathIncognito <> " hi") + ] + cath + <### [ ConsoleString ("#team " <> bobIncognito <> " is creating direct contact " <> bobIncognito <> " with you"), + WithTime ("i " <> bobIncognito <> "> hi") + ] + _ <- getTermLine bob + _ <- getTermLine cath + concurrentlyN_ + [ do + bob <## (cathIncognito <> ": contact is connected, your incognito profile for this contact is " <> bobIncognito) + bob <## ("use /i " <> cathIncognito <> " to print out this incognito profile again"), + do + cath <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> cathIncognito) + cath <## ("use /i " <> bobIncognito <> " to print out this incognito profile again") + ] + + bob `hasContactProfiles` ["bob", "alice", T.pack bobIncognito, T.pack cathIncognito] + cath `hasContactProfiles` ["cath", "alice", T.pack bobIncognito, T.pack cathIncognito] + + bob ?#> ("@" <> cathIncognito <> " hi, I'm incognito") + cath ?<# (bobIncognito <> "> hi, I'm incognito") + cath ?#> ("@" <> bobIncognito <> " hey, me too") + bob ?<# (cathIncognito <> "> hey, me too") + + -- members still use incognito profile for group + alice #> "#team hello" + concurrentlyN_ + [ bob ?<# "#team alice> hello", + cath ?<# "#team alice> hello" + ] + bob ?#> "#team hi there" + concurrentlyN_ + [ alice <# ("#team " <> bobIncognito <> "> hi there"), + cath ?<# ("#team " <> bobIncognito <> "> hi there") + ] + cath ?#> "#team hey" + concurrentlyN_ + [ alice <# ("#team " <> cathIncognito <> "> hey"), + bob ?<# ("#team " <> cathIncognito <> "> hey") + ] + +testMemberContactProfileUpdate :: HasCallStack => FilePath -> IO () +testMemberContactProfileUpdate = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + bob ##> "/p rob Rob" + bob <## "user profile is changed to rob (Rob) (your 1 contacts are notified)" + alice <## "contact bob changed to rob (Rob)" + alice <## "use @rob to send messages" + + cath ##> "/p kate Kate" + cath <## "user profile is changed to kate (Kate) (your 1 contacts are notified)" + alice <## "contact cath changed to kate (Kate)" + alice <## "use @kate to send messages" + + alice #> "#team hello" + bob <# "#team alice> hello" + cath <# "#team alice> hello" + + bob #> "#team hello too" + alice <# "#team rob> hello too" + cath <# "#team bob> hello too" -- not updated profile + + cath #> "#team hello there" + alice <# "#team kate> hello there" + bob <# "#team cath> hello there" -- not updated profile + + bob `send` "@cath hi" + bob + <### [ "member #team cath does not have direct connection, creating", + "contact for member #team cath is created", + "sent invitation to connect directly to member #team cath", + WithTime "@cath hi" + ] + cath + <### [ "#team bob is creating direct contact bob with you", + WithTime "bob> hi" + ] + concurrentlyN_ + [ do + bob <## "contact cath changed to kate (Kate)" + bob <## "use @kate to send messages" + bob <## "kate (Kate): contact is connected", + do + cath <## "contact bob changed to rob (Rob)" + cath <## "use @rob to send messages" + cath <## "rob (Rob): contact is connected" + ] + + bob ##> "/contacts" + bob + <### [ "alice (Alice)", + "kate (Kate)" + ] + cath ##> "/contacts" + cath + <### [ "alice (Alice)", + "rob (Rob)" + ] + alice `hasContactProfiles` ["alice", "rob", "kate"] + bob `hasContactProfiles` ["rob", "alice", "kate"] + cath `hasContactProfiles` ["kate", "alice", "rob"] + + bob #> "#team hello too" + alice <# "#team rob> hello too" + cath <# "#team rob> hello too" -- updated profile + + cath #> "#team hello there" + alice <# "#team kate> hello there" + bob <# "#team kate> hello there" -- updated profile diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index 08d33df1d6..1a2b74f76e 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -18,7 +18,7 @@ chatProfileTests = do it "update user profile and notify contacts" testUpdateProfile it "update user profile with image" testUpdateProfileImage describe "user contact link" $ do - describe "create and connect via contact link" testUserContactLink + it "create and connect via contact link" testUserContactLink it "add contact link to profile" testProfileLink it "auto accept contact requests" testUserContactLinkAutoAccept it "deduplicate contact requests" testDeduplicateContactRequests @@ -27,10 +27,15 @@ chatProfileTests = do it "delete connection requests when contact link deleted" testDeleteConnectionRequests it "auto-reply message" testAutoReplyMessage it "auto-reply message in incognito" testAutoReplyMessageInIncognito - describe "incognito mode" $ do + describe "incognito" $ do it "connect incognito via invitation link" testConnectIncognitoInvitationLink it "connect incognito via contact address" testConnectIncognitoContactAddress it "accept contact request incognito" testAcceptContactRequestIncognito + it "set connection incognito" testSetConnectionIncognito + it "reset connection incognito" testResetConnectionIncognito + it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation + it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors + it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito it "can't invite contact to whom user connected incognito to a group" testCantInviteContactIncognito it "can't see global preferences update" testCantSeeGlobalPrefsUpdateIncognito @@ -52,7 +57,7 @@ chatProfileTests = do testUpdateProfile :: HasCallStack => FilePath -> IO () testUpdateProfile = - testChat3 aliceProfile bobProfile cathProfile $ + testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ \alice bob cath -> do createGroup3 "team" alice bob cath alice ##> "/p" @@ -112,33 +117,35 @@ testUpdateProfileImage = bob <## "use @alice2 to send messages" (bob do - alice ##> "/ad" - cLink <- getContactLink alice True - bob ##> ("/c " <> cLink) - alice <#? bob - alice @@@ [("<@bob", "")] - alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." - concurrently_ - (bob <## "alice (Alice): contact is connected") - (alice <## "bob (Bob): contact is connected") - threadDelay 100000 - alice @@@ [("@bob", lastChatFeature)] - alice <##> bob +testUserContactLink :: HasCallStack => FilePath -> IO () +testUserContactLink = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + alice ##> "/ad" + cLink <- getContactLink alice True + bob ##> ("/c " <> cLink) + alice <#? bob + alice @@@ [("<@bob", "")] + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + threadDelay 100000 + alice @@@ [("@bob", lastChatFeature)] + alice <##> bob - cath ##> ("/c " <> cLink) - alice <#? cath - alice @@@ [("<@cath", ""), ("@bob", "hey")] - alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." - concurrently_ - (cath <## "alice (Alice): contact is connected") - (alice <## "cath (Catherine): contact is connected") - threadDelay 100000 - alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] - alice <##> cath + cath ##> ("/c " <> cLink) + alice <#? cath + alice @@@ [("<@cath", ""), ("@bob", "hey")] + alice ##> "/ac cath" + alice <## "cath (Catherine): accepting contact request..." + concurrently_ + (cath <## "alice (Alice): contact is connected") + (alice <## "cath (Catherine): contact is connected") + threadDelay 100000 + alice @@@ [("@cath", lastChatFeature), ("@bob", "hey")] + alice <##> cath testProfileLink :: HasCallStack => FilePath -> IO () testProfileLink = @@ -209,6 +216,7 @@ testProfileLink = cc <## ("contact address: " <> cLink) cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## currentChatVRangeInfo checkAliceNoProfileLink cc = do cc ##> "/info alice" cc <## "contact ID: 2" @@ -216,6 +224,7 @@ testProfileLink = cc <##. "sending messages via" cc <## "you've shared main profile with this contact" cc <## "connection not verified, use /code command to see security code" + cc <## currentChatVRangeInfo testUserContactLinkAutoAccept :: HasCallStack => FilePath -> IO () testUserContactLinkAutoAccept = @@ -489,11 +498,9 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ testConnectIncognitoInvitationLink :: HasCallStack => FilePath -> IO () testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - alice #$> ("/incognito on", id, "ok") - bob #$> ("/incognito on", id, "ok") - alice ##> "/c" + alice ##> "/connect incognito" inv <- getInvitation alice - bob ##> ("/c " <> inv) + bob ##> ("/connect incognito " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob aliceIncognito <- getTermLine alice @@ -505,9 +512,6 @@ testConnectIncognitoInvitationLink = testChat3 aliceProfile bobProfile cathProfi alice <## (bobIncognito <> ": contact is connected, your incognito profile for this contact is " <> aliceIncognito) alice <## ("use /i " <> bobIncognito <> " to print out this incognito profile again") ] - -- after turning incognito mode off conversation is incognito - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") alice ?#> ("@" <> bobIncognito <> " psst, I'm incognito") bob ?<# (aliceIncognito <> "> psst, I'm incognito") bob ?#> ("@" <> aliceIncognito <> " me too") @@ -569,8 +573,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/ad" cLink <- getContactLink alice True - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> cLink) + bob ##> ("/c i " <> cLink) bobIncognito <- getTermLine bob bob <## "connection request sent incognito!" alice <## (bobIncognito <> " wants to connect to you!") @@ -585,9 +588,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ bob <## "use /i alice to print out this incognito profile again", alice <## (bobIncognito <> ": contact is connected") ] - -- after turning incognito mode off conversation is incognito - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") + -- conversation is incognito alice #> ("@" <> bobIncognito <> " who are you?") bob ?<# "alice> who are you?" bob ?#> "@alice I'm Batman" @@ -605,237 +606,357 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ bob `hasContactProfiles` ["bob"] testAcceptContactRequestIncognito :: HasCallStack => FilePath -> IO () -testAcceptContactRequestIncognito = testChat2 aliceProfile bobProfile $ - \alice bob -> do +testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do alice ##> "/ad" cLink <- getContactLink alice True bob ##> ("/c " <> cLink) alice <#? bob - alice #$> ("/incognito on", id, "ok") - alice ##> "/ac bob" + alice ##> "/accept incognito bob" alice <## "bob (Bob): accepting contact request..." - aliceIncognito <- getTermLine alice + aliceIncognitoBob <- getTermLine alice concurrentlyN_ - [ bob <## (aliceIncognito <> ": contact is connected"), + [ bob <## (aliceIncognitoBob <> ": contact is connected"), do - alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognitoBob) alice <## "use /i bob to print out this incognito profile again" ] - -- after turning incognito mode off conversation is incognito - alice #$> ("/incognito off", id, "ok") - bob #$> ("/incognito off", id, "ok") + -- conversation is incognito alice ?#> "@bob my profile is totally inconspicuous" - bob <# (aliceIncognito <> "> my profile is totally inconspicuous") - bob #> ("@" <> aliceIncognito <> " I know!") + bob <# (aliceIncognitoBob <> "> my profile is totally inconspicuous") + bob #> ("@" <> aliceIncognitoBob <> " I know!") alice ?<# "bob> I know!" -- list contacts alice ##> "/contacts" alice <## "i bob (Bob)" - alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] + alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognitoBob] -- delete contact, incognito profile is deleted alice ##> "/d bob" alice <## "bob: contact is deleted" alice ##> "/contacts" (alice ("/c " <> cLink) + alice <#? cath + alice ##> "/_accept incognito=on 1" + alice <## "cath (Catherine): accepting contact request..." + aliceIncognitoCath <- getTermLine alice + concurrentlyN_ + [ cath <## (aliceIncognitoCath <> ": contact is connected"), + do + alice <## ("cath (Catherine): contact is connected, your incognito profile for this contact is " <> aliceIncognitoCath) + alice <## "use /i cath to print out this incognito profile again" + ] + alice `hasContactProfiles` ["alice", "cath", T.pack aliceIncognitoCath] + cath `hasContactProfiles` ["cath", T.pack aliceIncognitoCath] + +testSetConnectionIncognito :: HasCallStack => FilePath -> IO () +testSetConnectionIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/connect" + inv <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (aliceIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + alice ?#> ("@bob hi") + bob <# (aliceIncognito <> "> hi") + bob #> ("@" <> aliceIncognito <> " hey") + alice ?<# ("bob> hey") + alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] + bob `hasContactProfiles` ["bob", T.pack aliceIncognito] + +testResetConnectionIncognito :: HasCallStack => FilePath -> IO () +testResetConnectionIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/_connect 1 incognito=on" + inv <- getInvitation alice + alice ##> "/_set incognito :1 off" + alice <## "connection 1 changed to non incognito" + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testSetConnectionIncognitoProhibitedDuringNegotiation :: HasCallStack => FilePath -> IO () +testSetConnectionIncognitoProhibitedDuringNegotiation tmp = do + inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do + threadDelay 250000 + alice ##> "/connect" + getInvitation alice + withNewTestChat tmp "bob" bobProfile $ \bob -> do + threadDelay 250000 + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withTestChat tmp "alice" $ \alice -> do + threadDelay 250000 + alice ##> "/_set incognito :1 on" + alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" + withTestChat tmp "bob" $ \bob -> do + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testConnectionIncognitoUnchangedErrors :: HasCallStack => FilePath -> IO () +testConnectionIncognitoUnchangedErrors = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/connect" + inv <- getInvitation alice + alice ##> "/_set incognito :1 off" + alice <## "incognito mode change prohibited" + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + alice ##> "/_set incognito :1 on" + alice <## "incognito mode change prohibited" + alice ##> "/_set incognito :1 off" + alice <## "connection 1 changed to non incognito" + alice ##> "/_set incognito :1 off" + alice <## "incognito mode change prohibited" + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testSetResetSetConnectionIncognito :: HasCallStack => FilePath -> IO () +testSetResetSetConnectionIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/_connect 1 incognito=off" + inv <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + alice ##> "/_set incognito :1 off" + alice <## "connection 1 changed to non incognito" + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/_connect 1 incognito=off " <> inv) + bob <## "confirmation sent!" + aliceIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (aliceIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + alice ?#> ("@bob hi") + bob <# (aliceIncognito <> "> hi") + bob #> ("@" <> aliceIncognito <> " hey") + alice ?<# ("bob> hey") + alice `hasContactProfiles` ["alice", "bob", T.pack aliceIncognito] + bob `hasContactProfiles` ["bob", T.pack aliceIncognito] testJoinGroupIncognito :: HasCallStack => FilePath -> IO () -testJoinGroupIncognito = testChat4 aliceProfile bobProfile cathProfile danProfile $ - \alice bob cath dan -> do - -- non incognito connections - connectUsers alice bob - connectUsers alice dan - connectUsers bob cath - connectUsers bob dan - connectUsers cath dan - -- cath connected incognito to alice - alice ##> "/c" - inv <- getInvitation alice - cath #$> ("/incognito on", id, "ok") - cath ##> ("/c " <> inv) - cath <## "confirmation sent!" - cathIncognito <- getTermLine cath - concurrentlyN_ - [ do - cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito) - cath <## "use /i alice to print out this incognito profile again", - alice <## (cathIncognito <> ": contact is connected") - ] - -- alice creates group - alice ##> "/g secret_club" - alice <## "group #secret_club is created" - alice <## "to add members use /a secret_club or /create link #secret_club" - -- alice invites bob - alice ##> "/a secret_club bob" - concurrentlyN_ - [ alice <## "invitation to join the group #secret_club sent to bob", - do - bob <## "#secret_club: alice invites you to join the group as admin" - bob <## "use /j secret_club to accept" - ] - bob ##> "/j secret_club" - concurrently_ - (alice <## "#secret_club: bob joined the group") - (bob <## "#secret_club: you joined the group") - -- alice invites cath - alice ##> ("/a secret_club " <> cathIncognito) - concurrentlyN_ - [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito), - do - cath <## "#secret_club: alice invites you to join the group as admin" - cath <## ("use /j secret_club to join incognito as " <> cathIncognito) - ] - -- cath uses the same incognito profile when joining group, disabling incognito mode doesn't affect it - cath #$> ("/incognito off", id, "ok") - cath ##> "/j secret_club" - -- cath and bob don't merge contacts - concurrentlyN_ - [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"), - do - cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito) - cath <## "#secret_club: member bob_1 (Bob) is connected", - do - bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)") - bob <## ("#secret_club: new member " <> cathIncognito <> " is connected") - ] - -- cath cannot invite to the group because her membership is incognito - cath ##> "/a secret_club dan" - cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts" - -- alice invites dan - alice ##> "/a secret_club dan" - concurrentlyN_ - [ alice <## "invitation to join the group #secret_club sent to dan", - do - dan <## "#secret_club: alice invites you to join the group as admin" - dan <## "use /j secret_club to accept" - ] - dan ##> "/j secret_club" - -- cath and dan don't merge contacts - concurrentlyN_ - [ alice <## "#secret_club: dan joined the group", - do - dan <## "#secret_club: you joined the group" - dan - <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected", - "#secret_club: member bob_1 (Bob) is connected", - "contact bob_1 is merged into bob", - "use @bob to send messages" - ], - do - bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" - bob <## "#secret_club: new member dan_1 is connected" - bob <## "contact dan_1 is merged into dan" - bob <## "use @dan to send messages", - do - cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" - cath <## "#secret_club: new member dan_1 is connected" - ] - -- send messages - group is incognito for cath - alice #> "#secret_club hello" - concurrentlyN_ - [ bob <# "#secret_club alice> hello", - cath ?<# "#secret_club alice> hello", - dan <# "#secret_club alice> hello" - ] - bob #> "#secret_club hi there" - concurrentlyN_ - [ alice <# "#secret_club bob> hi there", - cath ?<# "#secret_club bob_1> hi there", - dan <# "#secret_club bob> hi there" - ] - cath ?#> "#secret_club hey" - concurrentlyN_ - [ alice <# ("#secret_club " <> cathIncognito <> "> hey"), - bob <# ("#secret_club " <> cathIncognito <> "> hey"), - dan <# ("#secret_club " <> cathIncognito <> "> hey") - ] - dan #> "#secret_club how is it going?" - concurrentlyN_ - [ alice <# "#secret_club dan> how is it going?", - bob <# "#secret_club dan> how is it going?", - cath ?<# "#secret_club dan_1> how is it going?" - ] - -- cath and bob can send messages via new direct connection, cath is incognito - bob #> ("@" <> cathIncognito <> " hi, I'm bob") - cath ?<# "bob_1> hi, I'm bob" - cath ?#> "@bob_1 hey, I'm incognito" - bob <# (cathIncognito <> "> hey, I'm incognito") - -- cath and dan can send messages via new direct connection, cath is incognito - dan #> ("@" <> cathIncognito <> " hi, I'm dan") - cath ?<# "dan_1> hi, I'm dan" - cath ?#> "@dan_1 hey, I'm incognito" - dan <# (cathIncognito <> "> hey, I'm incognito") - -- non incognito connections are separate - bob <##> cath - dan <##> cath - -- list groups - cath ##> "/gs" - cath <## "i #secret_club" - -- list group members - alice ##> "/ms secret_club" - alice - <### [ "alice (Alice): owner, you, created group", - "bob (Bob): admin, invited, connected", - ConsoleString $ cathIncognito <> ": admin, invited, connected", - "dan (Daniel): admin, invited, connected" - ] - bob ##> "/ms secret_club" - bob - <### [ "alice (Alice): owner, host, connected", - "bob (Bob): admin, you, connected", - ConsoleString $ cathIncognito <> ": admin, connected", - "dan (Daniel): admin, connected" - ] - cath ##> "/ms secret_club" - cath - <### [ "alice (Alice): owner, host, connected", - "bob_1 (Bob): admin, connected", - ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected", - "dan_1 (Daniel): admin, connected" - ] - dan ##> "/ms secret_club" - dan - <### [ "alice (Alice): owner, host, connected", - "bob (Bob): admin, connected", - ConsoleString $ cathIncognito <> ": admin, connected", - "dan (Daniel): admin, you, connected" - ] - -- remove member - bob ##> ("/rm secret_club " <> cathIncognito) - concurrentlyN_ - [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), - alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), - dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), - do - cath <## "#secret_club: bob_1 removed you from the group" - cath <## "use /d #secret_club to delete the group" - ] - bob #> "#secret_club hi" - concurrentlyN_ - [ alice <# "#secret_club bob> hi", - dan <# "#secret_club bob> hi", - (cath "#secret_club hello" - concurrentlyN_ - [ bob <# "#secret_club alice> hello", - dan <# "#secret_club alice> hello", - (cath "#secret_club hello" - cath <## "you are no longer a member of the group" - -- cath can still message members directly - bob #> ("@" <> cathIncognito <> " I removed you from group") - cath ?<# "bob_1> I removed you from group" - cath ?#> "@bob_1 ok" - bob <# (cathIncognito <> "> ok") +testJoinGroupIncognito = + testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + -- non incognito connections + connectUsers alice bob + connectUsers alice dan + connectUsers bob cath + connectUsers bob dan + connectUsers cath dan + -- cath connected incognito to alice + alice ##> "/c" + inv <- getInvitation alice + cath ##> ("/c i " <> inv) + cath <## "confirmation sent!" + cathIncognito <- getTermLine cath + concurrentlyN_ + [ do + cath <## ("alice (Alice): contact is connected, your incognito profile for this contact is " <> cathIncognito) + cath <## "use /i alice to print out this incognito profile again", + alice <## (cathIncognito <> ": contact is connected") + ] + -- alice creates group + alice ##> "/g secret_club" + alice <## "group #secret_club is created" + alice <## "to add members use /a secret_club or /create link #secret_club" + -- alice invites bob + alice ##> "/a secret_club bob admin" + concurrentlyN_ + [ alice <## "invitation to join the group #secret_club sent to bob", + do + bob <## "#secret_club: alice invites you to join the group as admin" + bob <## "use /j secret_club to accept" + ] + bob ##> "/j secret_club" + concurrently_ + (alice <## "#secret_club: bob joined the group") + (bob <## "#secret_club: you joined the group") + -- alice invites cath + alice ##> ("/a secret_club " <> cathIncognito <> " admin") + concurrentlyN_ + [ alice <## ("invitation to join the group #secret_club sent to " <> cathIncognito), + do + cath <## "#secret_club: alice invites you to join the group as admin" + cath <## ("use /j secret_club to join incognito as " <> cathIncognito) + ] + -- cath uses the same incognito profile when joining group, cath and bob don't merge contacts + cath ##> "/j secret_club" + concurrentlyN_ + [ alice <## ("#secret_club: " <> cathIncognito <> " joined the group"), + do + cath <## ("#secret_club: you joined the group incognito as " <> cathIncognito) + cath <## "#secret_club: member bob_1 (Bob) is connected", + do + bob <## ("#secret_club: alice added " <> cathIncognito <> " to the group (connecting...)") + bob <## ("#secret_club: new member " <> cathIncognito <> " is connected") + ] + -- cath cannot invite to the group because her membership is incognito + cath ##> "/a secret_club dan" + cath <## "you've connected to this group using an incognito profile - prohibited to invite contacts" + -- alice invites dan + alice ##> "/a secret_club dan admin" + concurrentlyN_ + [ alice <## "invitation to join the group #secret_club sent to dan", + do + dan <## "#secret_club: alice invites you to join the group as admin" + dan <## "use /j secret_club to accept" + ] + dan ##> "/j secret_club" + -- cath and dan don't merge contacts + concurrentlyN_ + [ alice <## "#secret_club: dan joined the group", + do + dan <## "#secret_club: you joined the group" + dan + <### [ ConsoleString $ "#secret_club: member " <> cathIncognito <> " is connected", + "#secret_club: member bob_1 (Bob) is connected", + "contact bob_1 is merged into bob", + "use @bob to send messages" + ], + do + bob <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" + bob <## "#secret_club: new member dan_1 is connected" + bob <## "contact dan_1 is merged into dan" + bob <## "use @dan to send messages", + do + cath <## "#secret_club: alice added dan_1 (Daniel) to the group (connecting...)" + cath <## "#secret_club: new member dan_1 is connected" + ] + -- send messages - group is incognito for cath + alice #> "#secret_club hello" + concurrentlyN_ + [ bob <# "#secret_club alice> hello", + cath ?<# "#secret_club alice> hello", + dan <# "#secret_club alice> hello" + ] + bob #> "#secret_club hi there" + concurrentlyN_ + [ alice <# "#secret_club bob> hi there", + cath ?<# "#secret_club bob_1> hi there", + dan <# "#secret_club bob> hi there" + ] + cath ?#> "#secret_club hey" + concurrentlyN_ + [ alice <# ("#secret_club " <> cathIncognito <> "> hey"), + bob <# ("#secret_club " <> cathIncognito <> "> hey"), + dan <# ("#secret_club " <> cathIncognito <> "> hey") + ] + dan #> "#secret_club how is it going?" + concurrentlyN_ + [ alice <# "#secret_club dan> how is it going?", + bob <# "#secret_club dan> how is it going?", + cath ?<# "#secret_club dan_1> how is it going?" + ] + -- cath and bob can send messages via new direct connection, cath is incognito + bob #> ("@" <> cathIncognito <> " hi, I'm bob") + cath ?<# "bob_1> hi, I'm bob" + cath ?#> "@bob_1 hey, I'm incognito" + bob <# (cathIncognito <> "> hey, I'm incognito") + -- cath and dan can send messages via new direct connection, cath is incognito + dan #> ("@" <> cathIncognito <> " hi, I'm dan") + cath ?<# "dan_1> hi, I'm dan" + cath ?#> "@dan_1 hey, I'm incognito" + dan <# (cathIncognito <> "> hey, I'm incognito") + -- non incognito connections are separate + bob <##> cath + dan <##> cath + -- list groups + cath ##> "/gs" + cath <## "i #secret_club (4 members)" + -- list group members + alice ##> "/ms secret_club" + alice + <### [ "alice (Alice): owner, you, created group", + "bob (Bob): admin, invited, connected", + ConsoleString $ cathIncognito <> ": admin, invited, connected", + "dan (Daniel): admin, invited, connected" + ] + bob ##> "/ms secret_club" + bob + <### [ "alice (Alice): owner, host, connected", + "bob (Bob): admin, you, connected", + ConsoleString $ cathIncognito <> ": admin, connected", + "dan (Daniel): admin, connected" + ] + cath ##> "/ms secret_club" + cath + <### [ "alice (Alice): owner, host, connected", + "bob_1 (Bob): admin, connected", + ConsoleString $ "i " <> cathIncognito <> ": admin, you, connected", + "dan_1 (Daniel): admin, connected" + ] + dan ##> "/ms secret_club" + dan + <### [ "alice (Alice): owner, host, connected", + "bob (Bob): admin, connected", + ConsoleString $ cathIncognito <> ": admin, connected", + "dan (Daniel): admin, you, connected" + ] + -- remove member + bob ##> ("/rm secret_club " <> cathIncognito) + concurrentlyN_ + [ bob <## ("#secret_club: you removed " <> cathIncognito <> " from the group"), + alice <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), + dan <## ("#secret_club: bob removed " <> cathIncognito <> " from the group"), + do + cath <## "#secret_club: bob_1 removed you from the group" + cath <## "use /d #secret_club to delete the group" + ] + bob #> "#secret_club hi" + concurrentlyN_ + [ alice <# "#secret_club bob> hi", + dan <# "#secret_club bob> hi", + (cath "#secret_club hello" + concurrentlyN_ + [ bob <# "#secret_club alice> hello", + dan <# "#secret_club alice> hello", + (cath "#secret_club hello" + cath <## "you are no longer a member of the group" + -- cath can still message members directly + bob #> ("@" <> cathIncognito <> " I removed you from group") + cath ?<# "bob_1> I removed you from group" + cath ?#> "@bob_1 ok" + bob <# (cathIncognito <> "> ok") testCantInviteContactIncognito :: HasCallStack => FilePath -> IO () testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do -- alice connected incognito to bob - alice #$> ("/incognito on", id, "ok") - alice ##> "/c" + alice ##> "/c i" inv <- getInvitation alice bob ##> ("/c " <> inv) bob <## "confirmation sent!" @@ -847,7 +968,6 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ alice <## "use /i bob to print out this incognito profile again" ] -- alice creates group non incognito - alice #$> ("/incognito off", id, "ok") alice ##> "/g club" alice <## "group #club is created" alice <## "to add members use /a club or /create link #club" @@ -859,10 +979,8 @@ testCantInviteContactIncognito = testChat2 aliceProfile bobProfile $ testCantSeeGlobalPrefsUpdateIncognito :: HasCallStack => FilePath -> IO () testCantSeeGlobalPrefsUpdateIncognito = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do - alice #$> ("/incognito on", id, "ok") - alice ##> "/c" + alice ##> "/c i" invIncognito <- getInvitation alice - alice #$> ("/incognito off", id, "ok") alice ##> "/c" inv <- getInvitation alice bob ##> ("/c " <> invIncognito) @@ -915,8 +1033,7 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr -- bob connects incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) + bob ##> ("/c i " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob concurrentlyN_ @@ -933,7 +1050,7 @@ testDeleteContactThenGroupDeletesIncognitoProfile = testChat2 aliceProfile bobPr concurrentlyN_ [ alice <## ("invitation to join the group #team sent to " <> bobIncognito), do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## ("use /j team to join incognito as " <> bobIncognito) ] bob ##> "/j team" @@ -967,8 +1084,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr -- bob connects incognito to alice alice ##> "/c" inv <- getInvitation alice - bob #$> ("/incognito on", id, "ok") - bob ##> ("/c " <> inv) + bob ##> ("/c i " <> inv) bob <## "confirmation sent!" bobIncognito <- getTermLine bob concurrentlyN_ @@ -985,7 +1101,7 @@ testDeleteGroupThenContactDeletesIncognitoProfile = testChat2 aliceProfile bobPr concurrentlyN_ [ alice <## ("invitation to join the group #team sent to " <> bobIncognito), do - bob <## "#team: alice invites you to join the group as admin" + bob <## "#team: alice invites you to join the group as member" bob <## ("use /j team to join incognito as " <> bobIncognito) ] bob ##> "/j team" @@ -1243,54 +1359,55 @@ testAllowFullDeletionGroup = testProhibitDirectMessages :: HasCallStack => FilePath -> IO () testProhibitDirectMessages = - testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do - createGroup3 "team" alice bob cath - threadDelay 1000000 - alice ##> "/set direct #team off" - alice <## "updated group preferences:" - alice <## "Direct messages: off" - directProhibited bob - directProhibited cath - threadDelay 1000000 - -- still can send direct messages to direct contacts - alice #> "@bob hello again" - bob <# "alice> hello again" - alice #> "@cath hello again" - cath <# "alice> hello again" - bob ##> "@cath hello again" - bob <## "direct messages to indirect contact cath are prohibited" - (cath "/j #team" - concurrentlyN_ - [ cath <## "#team: dan joined the group", - do - dan <## "#team: you joined the group" - dan - <### [ "#team: member alice (Alice) is connected", - "#team: member bob (Bob) is connected" - ], - do - alice <## "#team: cath added dan (Daniel) to the group (connecting...)" - alice <## "#team: new member dan is connected", - do - bob <## "#team: cath added dan (Daniel) to the group (connecting...)" - bob <## "#team: new member dan is connected" - ] - alice ##> "@dan hi" - alice <## "direct messages to indirect contact dan are prohibited" - bob ##> "@dan hi" - bob <## "direct messages to indirect contact dan are prohibited" - (dan "@alice hi" - dan <## "direct messages to indirect contact alice are prohibited" - dan ##> "@bob hi" - dan <## "direct messages to indirect contact bob are prohibited" - dan #> "@cath hi" - cath <# "dan> hi" - cath #> "@dan hi" - dan <# "cath> hi" + testChatCfg4 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile danProfile $ + \alice bob cath dan -> do + createGroup3 "team" alice bob cath + threadDelay 1000000 + alice ##> "/set direct #team off" + alice <## "updated group preferences:" + alice <## "Direct messages: off" + directProhibited bob + directProhibited cath + threadDelay 1000000 + -- still can send direct messages to direct contacts + alice #> "@bob hello again" + bob <# "alice> hello again" + alice #> "@cath hello again" + cath <# "alice> hello again" + bob ##> "@cath hello again" + bob <## "direct messages to indirect contact cath are prohibited" + (cath "/j #team" + concurrentlyN_ + [ cath <## "#team: dan joined the group", + do + dan <## "#team: you joined the group" + dan + <### [ "#team: member alice (Alice) is connected", + "#team: member bob (Bob) is connected" + ], + do + alice <## "#team: cath added dan (Daniel) to the group (connecting...)" + alice <## "#team: new member dan is connected", + do + bob <## "#team: cath added dan (Daniel) to the group (connecting...)" + bob <## "#team: new member dan is connected" + ] + alice ##> "@dan hi" + alice <## "direct messages to indirect contact dan are prohibited" + bob ##> "@dan hi" + bob <## "direct messages to indirect contact dan are prohibited" + (dan "@alice hi" + dan <## "direct messages to indirect contact alice are prohibited" + dan ##> "@bob hi" + dan <## "direct messages to indirect contact bob are prohibited" + dan #> "@cath hi" + cath <# "dan> hi" + cath #> "@dan hi" + dan <# "cath> hi" where directProhibited :: HasCallStack => TestCC -> IO () directProhibited cc = do diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index c6acf986a1..c120d661ff 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -18,11 +18,13 @@ import Data.Maybe (fromMaybe) import Data.String import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), InlineFilesConfig (..), defaultInlineFilesConfig) +import Simplex.Chat.Protocol import Simplex.Chat.Store.Profiles (getUserContactProfiles) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Messaging.Agent.Store.SQLite (withTransaction) import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Version import System.Directory (doesFileExist) import System.Environment (lookupEnv) import System.FilePath (()) @@ -48,9 +50,15 @@ xit' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a) xit' = if os == "linux" then xit else it xit'' :: (HasCallStack, Example a) => String -> a -> SpecWith (Arg a) -xit'' d t = do +xit'' = ifCI xit it + +xdescribe'' :: HasCallStack => String -> SpecWith a -> SpecWith a +xdescribe'' = ifCI xdescribe describe + +ifCI :: HasCallStack => (HasCallStack => String -> a -> SpecWith b) -> (HasCallStack => String -> a -> SpecWith b) -> String -> a -> SpecWith b +ifCI xrun run d t = do ci <- runIO $ lookupEnv "CI" - (if ci == Just "true" then xit else it) d t + (if ci == Just "true" then xrun else run) d t versionTestMatrix2 :: (HasCallStack => TestCC -> TestCC -> IO ()) -> SpecWith FilePath versionTestMatrix2 runTest = do @@ -59,9 +67,9 @@ versionTestMatrix2 runTest = do it "v1 to v2" $ runTestCfg2 testCfg testCfgV1 runTest it "v2 to v1" $ runTestCfg2 testCfgV1 testCfg runTest -versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath -versionTestMatrix3 runTest = do - it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest +-- versionTestMatrix3 :: (HasCallStack => TestCC -> TestCC -> TestCC -> IO ()) -> SpecWith FilePath +-- versionTestMatrix3 runTest = do +-- it "v2" $ testChat3 aliceProfile bobProfile cathProfile runTest -- it "v1" $ testChatCfg3 testCfgV1 aliceProfile bobProfile cathProfile runTest -- it "v1 to v2" $ runTestCfg3 testCfg testCfgV1 testCfgV1 runTest @@ -311,6 +319,9 @@ cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line (⩗) :: HasCallStack => TestCC -> String -> Expectation cc ⩗ line = (dropTime . dropReceipt <$> getTermLine cc) `shouldReturn` line +(%) :: HasCallStack => TestCC -> String -> Expectation +cc % line = (dropTime . dropPartialReceipt <$> getTermLine cc) `shouldReturn` line + ( TestCC -> Expectation ( Nothing +dropStrPrefix :: HasCallStack => String -> String -> String +dropStrPrefix pfx s = + let (p, rest) = splitAt (length pfx) s + in if p == pfx then rest else error $ "no prefix " <> pfx <> " in string : " <> s + dropReceipt :: HasCallStack => String -> String dropReceipt msg = fromMaybe err $ dropReceipt_ msg where @@ -356,6 +372,16 @@ dropReceipt_ msg = case splitAt 2 msg of ("⩗ ", text) -> Just text _ -> Nothing +dropPartialReceipt :: HasCallStack => String -> String +dropPartialReceipt msg = fromMaybe err $ dropPartialReceipt_ msg + where + err = error $ "invalid partial receipt: " <> msg + +dropPartialReceipt_ :: String -> Maybe String +dropPartialReceipt_ msg = case splitAt 2 msg of + ("% ", text) -> Just text + _ -> Nothing + getInvitation :: HasCallStack => TestCC -> IO String getInvitation cc = do cc <## "pass this invitation link to your contact (via another channel):" @@ -446,6 +472,7 @@ createGroup3 :: HasCallStack => String -> TestCC -> TestCC -> TestCC -> IO () createGroup3 gName cc1 cc2 cc3 = do createGroup2 gName cc1 cc2 connectUsers cc1 cc3 + name1 <- userName cc1 name3 <- userName cc3 sName2 <- showName cc2 sName3 <- showName cc3 @@ -457,19 +484,23 @@ createGroup3 gName cc1 cc2 cc3 = do cc3 <## ("#" <> gName <> ": you joined the group") cc3 <## ("#" <> gName <> ": member " <> sName2 <> " is connected"), do - cc2 <## ("#" <> gName <> ": alice added " <> sName3 <> " to the group (connecting...)") + cc2 <## ("#" <> gName <> ": " <> name1 <> " added " <> sName3 <> " to the group (connecting...)") cc2 <## ("#" <> gName <> ": new member " <> name3 <> " is connected") ] addMember :: HasCallStack => String -> TestCC -> TestCC -> GroupMemberRole -> IO () -addMember gName inviting invitee role = do +addMember gName = fullAddMember gName "" + +fullAddMember :: HasCallStack => String -> String -> TestCC -> TestCC -> GroupMemberRole -> IO () +fullAddMember gName fullName inviting invitee role = do name1 <- userName inviting memName <- userName invitee inviting ##> ("/a " <> gName <> " " <> memName <> " " <> B.unpack (strEncode role)) + let fullName' = if null fullName || fullName == gName then "" else " (" <> fullName <> ")" concurrentlyN_ [ inviting <## ("invitation to join the group #" <> gName <> " sent to " <> memName), do - invitee <## ("#" <> gName <> ": " <> name1 <> " invites you to join the group as " <> B.unpack (strEncode role)) + invitee <## ("#" <> gName <> fullName' <> ": " <> name1 <> " invites you to join the group as " <> B.unpack (strEncode role)) invitee <## ("use /j " <> gName <> " to accept") ] @@ -494,3 +525,10 @@ startFileTransferWithDest' cc1 cc2 fileName fileSize fileDest_ = do concurrently_ (cc2 <## ("started receiving file 1 (" <> fileName <> ") from " <> name1)) (cc1 <## ("started sending file 1 (" <> fileName <> ") to " <> name2)) + +currentChatVRangeInfo :: String +currentChatVRangeInfo = + "peer chat protocol version range: " <> vRangeStr supportedChatVRange + +vRangeStr :: VersionRange -> String +vRangeStr (VersionRange minVer maxVer) = "(" <> show minVer <> ", " <> show maxVer <> ")" diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index 220c8a0d3e..0b965250b4 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -1,83 +1,127 @@ {-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +{-# OPTIONS_GHC -fno-warn-orphans #-} module MobileTests where import ChatTests.Utils import Control.Monad.Except +import Crypto.Random (getRandomBytes) +import Data.Aeson (FromJSON (..)) +import qualified Data.Aeson as J +import Data.ByteString (ByteString) +import qualified Data.ByteString as B +import qualified Data.ByteString.Char8 as BS +import Data.ByteString.Internal (create, memcpy) +import qualified Data.ByteString.Lazy.Char8 as LB +import Data.Word (Word8, Word32) +import Foreign.C +import Foreign.Marshal.Alloc (mallocBytes) +import Foreign.Ptr +import Foreign.Storable (peek) +import GHC.IO.Encoding (setLocaleEncoding, setFileSystemEncoding, setForeignEncoding) import Simplex.Chat.Mobile +import Simplex.Chat.Mobile.File +import Simplex.Chat.Mobile.Shared +import Simplex.Chat.Mobile.WebRTC import Simplex.Chat.Store import Simplex.Chat.Store.Profiles import Simplex.Chat.Types (AgentUserId (..), Profile (..)) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..)) +import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.File (CryptoFile(..), CryptoFileArgs (..)) +import qualified Simplex.Messaging.Crypto.File as CF +import Simplex.Messaging.Encoding.String +import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON) +import System.Directory (copyFile) import System.FilePath (()) +import System.IO (utf8) import Test.Hspec -mobileTests :: SpecWith FilePath +mobileTests :: HasCallStack => SpecWith FilePath mobileTests = do describe "mobile API" $ do + runIO $ do + setLocaleEncoding utf8 + setFileSystemEncoding utf8 + setForeignEncoding utf8 it "start new chat without user" testChatApiNoUser it "start new chat with existing user" testChatApi + it "should encrypt/decrypt WebRTC frames" testMediaApi + it "should encrypt/decrypt WebRTC frames via C API" testMediaCApi + describe "should read/write encrypted files via C API" $ do + it "latin1 name" $ testFileCApi "test" + it "utf8 name 1" $ testFileCApi "тест" + it "utf8 name 2" $ testFileCApi "👍" + it "no exception on missing file" testMissingFileCApi + describe "should encrypt/decrypt files via C API" $ do + it "latin1 name" $ testFileEncryptionCApi "test" + it "utf8 name 1" $ testFileEncryptionCApi "тест" + it "utf8 name 2" $ testFileEncryptionCApi "👍" + it "no exception on missing file" testMissingFileEncryptionCApi -noActiveUser :: String +noActiveUser :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) noActiveUser = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"noActiveUser\":{}}}}}}}" #else noActiveUser = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}" #endif -activeUserExists :: String +activeUserExists :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) -activeUserExists = "{\"resp\":{\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":false},\"chatError\":{\"error\":{\"errorType\":{\"userExists\":{\"contactName\":\"alice\"}}}}}}}" +activeUserExists = "{\"resp\":{\"chatCmdError\":{\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"error\":{\"errorType\":{\"userExists\":{\"contactName\":\"alice\"}}}}}}}" #else -activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":false},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}" +activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"user_\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true},\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"userExists\",\"contactName\":\"alice\"}}}}" #endif -activeUser :: String +activeUser :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) -activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":false}}}}" +activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}}" #else -activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":false}}}" +activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}}}" #endif -chatStarted :: String +chatStarted :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) chatStarted = "{\"resp\":{\"chatStarted\":{}}}" #else chatStarted = "{\"resp\":{\"type\":\"chatStarted\"}}" #endif -contactSubSummary :: String +contactSubSummary :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) contactSubSummary = "{\"resp\":{\"contactSubSummary\":{" <> userJSON <> ",\"contactSubscriptions\":[]}}}" #else contactSubSummary = "{\"resp\":{\"type\":\"contactSubSummary\"," <> userJSON <> ",\"contactSubscriptions\":[]}}" #endif -memberSubSummary :: String +memberSubSummary :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) memberSubSummary = "{\"resp\":{\"memberSubSummary\":{" <> userJSON <> ",\"memberSubscriptions\":[]}}}" #else memberSubSummary = "{\"resp\":{\"type\":\"memberSubSummary\"," <> userJSON <> ",\"memberSubscriptions\":[]}}" #endif -userContactSubSummary :: String +userContactSubSummary :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) userContactSubSummary = "{\"resp\":{\"userContactSubSummary\":{" <> userJSON <> ",\"userContactSubscriptions\":[]}}}" #else userContactSubSummary = "{\"resp\":{\"type\":\"userContactSubSummary\"," <> userJSON <> ",\"userContactSubscriptions\":[]}}" #endif -pendingSubSummary :: String +pendingSubSummary :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) pendingSubSummary = "{\"resp\":{\"pendingSubSummary\":{" <> userJSON <> ",\"pendingSubscriptions\":[]}}}" #else pendingSubSummary = "{\"resp\":{\"type\":\"pendingSubSummary\"," <> userJSON <> ",\"pendingSubscriptions\":[]}}" #endif -userJSON :: String -userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":false}" +userJSON :: LB.ByteString +userJSON = "\"user\":{\"userId\":1,\"agentUserId\":\"1\",\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"profileId\":1,\"displayName\":\"alice\",\"fullName\":\"Alice\",\"localAlias\":\"\"},\"fullPreferences\":{\"timedMessages\":{\"allow\":\"yes\"},\"fullDelete\":{\"allow\":\"no\"},\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"},\"calls\":{\"allow\":\"yes\"}},\"activeUser\":true,\"showNtfs\":true,\"sendRcptsContacts\":true,\"sendRcptsSmallGroups\":true}" -parsedMarkdown :: String +parsedMarkdown :: LB.ByteString #if defined(darwin_HOST_OS) && defined(swiftJSON) parsedMarkdown = "{\"formattedText\":[{\"format\":{\"bold\":{}},\"text\":\"hello\"}]}" #else @@ -113,3 +157,117 @@ testChatApi tmp = do chatRecvMsgWait cc 10000 `shouldReturn` "" chatParseMarkdown "hello" `shouldBe` "{}" chatParseMarkdown "*hello*" `shouldBe` parsedMarkdown + +testMediaApi :: HasCallStack => FilePath -> IO () +testMediaApi _ = do + key :: ByteString <- getRandomBytes 32 + frame <- getRandomBytes 100 + let keyStr = strEncode key + reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0 + frame' = frame <> reserved + Right encrypted <- runExceptT $ chatEncryptMedia keyStr frame' + encrypted `shouldNotBe` frame' + B.length encrypted `shouldBe` B.length frame' + runExceptT (chatDecryptMedia keyStr encrypted) `shouldReturn` Right frame' + +testMediaCApi :: HasCallStack => FilePath -> IO () +testMediaCApi _ = do + key :: ByteString <- getRandomBytes 32 + frame <- getRandomBytes 100 + let keyStr = strEncode key + reserved = B.replicate (C.authTagSize + C.gcmIVSize) 0 + frame' = frame <> reserved + encrypted <- test cChatEncryptMedia keyStr frame' + encrypted `shouldNotBe` frame' + test cChatDecryptMedia keyStr encrypted `shouldReturn` frame' + where + test :: HasCallStack => (CString -> Ptr Word8 -> CInt -> IO CString) -> ByteString -> ByteString -> IO ByteString + test f keyStr frame = do + let len = B.length frame + cLen = fromIntegral len + ptr <- mallocBytes len + putByteString ptr frame + cKeyStr <- newCAString $ BS.unpack keyStr + (f cKeyStr ptr cLen >>= peekCAString) `shouldReturn` "" + getByteString ptr cLen + +instance FromJSON WriteFileResult where parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "WF" + +instance FromJSON ReadFileResult where parseJSON = J.genericParseJSON . sumTypeJSON $ dropPrefix "RF" + +testFileCApi :: FilePath -> FilePath -> IO () +testFileCApi fileName tmp = do + src <- B.readFile "./tests/fixtures/test.pdf" + let path = tmp (fileName <> ".pdf") + cPath <- newCString path + let len = B.length src + cLen = fromIntegral len + ptr <- mallocBytes $ B.length src + putByteString ptr src + r <- peekCAString =<< cChatWriteFile cPath ptr cLen + Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r + let encryptedFile = CryptoFile path $ Just cfArgs + CF.getFileContentsSize encryptedFile `shouldReturn` fromIntegral (B.length src) + cKey <- encodedCString key + cNonce <- encodedCString nonce + -- the returned pointer contains 0, buffer length as Word32, then buffer + ptr' <- cChatReadFile cPath cKey cNonce + peek ptr' `shouldReturn` (0 :: Word8) + sz :: Word32 <- peek (ptr' `plusPtr` 1) + let sz' = fromIntegral sz + contents <- create sz' $ \toPtr -> memcpy toPtr (ptr' `plusPtr` 5) sz' + contents `shouldBe` src + sz' `shouldBe` fromIntegral len + +testMissingFileCApi :: FilePath -> IO () +testMissingFileCApi tmp = do + let path = tmp "missing_file" + cPath <- newCString path + CFArgs key nonce <- CF.randomArgs + cKey <- encodedCString key + cNonce <- encodedCString nonce + ptr <- cChatReadFile cPath cKey cNonce + peek ptr `shouldReturn` 1 + err <- peekCAString (ptr `plusPtr` 1) + err `shouldContain` "missing_file: openBinaryFile: does not exist" + +testFileEncryptionCApi :: FilePath -> FilePath -> IO () +testFileEncryptionCApi fileName tmp = do + let fromPath = tmp (fileName <> ".source.pdf") + copyFile "./tests/fixtures/test.pdf" fromPath + src <- B.readFile fromPath + cFromPath <- newCString fromPath + let toPath = tmp (fileName <> ".encrypted.pdf") + cToPath <- newCString toPath + r <- peekCAString =<< cChatEncryptFile cFromPath cToPath + Just (WFResult cfArgs@(CFArgs key nonce)) <- jDecode r + CF.getFileContentsSize (CryptoFile toPath $ Just cfArgs) `shouldReturn` fromIntegral (B.length src) + cKey <- encodedCString key + cNonce <- encodedCString nonce + let toPath' = tmp (fileName <> ".decrypted.pdf") + cToPath' <- newCString toPath' + "" <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath' + B.readFile toPath' `shouldReturn` src + +testMissingFileEncryptionCApi :: FilePath -> IO () +testMissingFileEncryptionCApi tmp = do + let fromPath = tmp "missing_file.source.pdf" + toPath = tmp "missing_file.encrypted.pdf" + cFromPath <- newCString fromPath + cToPath <- newCString toPath + r <- peekCAString =<< cChatEncryptFile cFromPath cToPath + Just (WFError err) <- jDecode r + err `shouldContain` fromPath + CFArgs key nonce <- CF.randomArgs + cKey <- encodedCString key + cNonce <- encodedCString nonce + let toPath' = tmp "missing_file.decrypted.pdf" + cToPath' <- newCString toPath' + err' <- peekCAString =<< cChatDecryptFile cToPath cKey cNonce cToPath' + err' `shouldContain` toPath + +jDecode :: FromJSON a => String -> IO (Maybe a) +jDecode = pure . J.decode . LB.pack + +encodedCString :: StrEncoding a => a -> IO CString +encodedCString = newCAString . BS.unpack . strEncode diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index 6f7e0b8cf4..d62d7a470a 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -76,10 +76,10 @@ s ##==## msg = do s ==## msg (==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation -s ==# msg = s ==## ChatMessage Nothing msg +s ==# msg = s ==## ChatMessage chatInitialVRange Nothing msg (#==) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation -s #== msg = s ##== ChatMessage Nothing msg +s #== msg = s ##== ChatMessage chatInitialVRange Nothing msg (#==#) :: MsgEncodingI e => ByteString -> ChatMsgEvent e -> Expectation s #==# msg = do @@ -101,59 +101,66 @@ testGroupProfile = GroupProfile {displayName = "team", fullName = "Team", descri decodeChatMessageTest :: Spec decodeChatMessageTest = describe "Chat message encoding/decoding" $ do it "x.msg.new simple text" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)) it "x.msg.new simple text - timed message TTL" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"ttl\":3600}}" #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing)) it "x.msg.new simple text - live message" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"live\":true}}" #==# XMsgNew (MCSimple (ExtMsgContent (MCText "hello") Nothing Nothing (Just True))) it "x.msg.new simple link" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"https://simplex.chat\",\"type\":\"link\",\"preview\":{\"description\":\"SimpleX Chat\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA\",\"title\":\"SimpleX Chat\",\"uri\":\"https://simplex.chat\"}}}}" #==# XMsgNew (MCSimple (extMsgContent (MCLink "https://simplex.chat" $ LinkPreview {uri = "https://simplex.chat", title = "SimpleX Chat", description = "SimpleX Chat", image = ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgA", content = Nothing}) Nothing)) it "x.msg.new simple image" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" #==# XMsgNew (MCSimple (extMsgContent (MCImage "" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing)) it "x.msg.new simple image with text" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"here's an image\",\"type\":\"image\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\"}}}" #==# XMsgNew (MCSimple (extMsgContent (MCImage "here's an image" $ ImageData "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=") Nothing)) - it "x.msg.new chat message " $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" - ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) + it "x.msg.new chat message" $ + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) + it "x.msg.new chat message with chat version range" $ + "{\"v\":\"1-2\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" ##==## ChatMessage + chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCQuote quotedMsg (extMsgContent (MCText "hello to you too") Nothing))) it "x.msg.new quote - timed message TTL" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}" + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"ttl\":3600}}" ##==## ChatMessage + chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing (Just 3600) Nothing))) it "x.msg.new quote - live message" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}" + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"live\":true}}" ##==## ChatMessage + chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCQuote quotedMsg (ExtMsgContent (MCText "hello to you too") Nothing Nothing (Just True)))) it "x.msg.new forward" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" - ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing)) + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") Nothing)) it "x.msg.new forward - timed message TTL" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" - ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing)) + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"ttl\":3600}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing (Just 3600) Nothing)) it "x.msg.new forward - live message" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" - ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True))) + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"live\":true}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (ExtMsgContent (MCText "hello") Nothing Nothing (Just True))) it "x.msg.new simple text with file" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XMsgNew (MCSimple (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) it "x.msg.new simple file with file" $ - "{\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}" + "{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"\",\"type\":\"file\"},\"file\":{\"fileSize\":12345,\"fileName\":\"file.txt\"}}}" #==# XMsgNew (MCSimple (extMsgContent (MCFile "") (Just FileInvitation {fileName = "file.txt", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) it "x.msg.new quote with file" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}},\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" ##==## ChatMessage + chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") ( XMsgNew ( MCQuote @@ -165,101 +172,119 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do ) ) it "x.msg.new forward with file" $ - "{\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" - ##==## ChatMessage (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) + "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"forward\":true,\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew $ MCForward (extMsgContent (MCText "hello") (Just FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing}))) it "x.msg.update" $ - "{\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1\",\"event\":\"x.msg.update\",\"params\":{\"msgId\":\"AQIDBA==\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XMsgUpdate (SharedMsgId "\1\2\3\4") (MCText "hello") Nothing Nothing it "x.msg.del" $ - "{\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.msg.del\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XMsgDel (SharedMsgId "\1\2\3\4") Nothing it "x.msg.deleted" $ - "{\"event\":\"x.msg.deleted\",\"params\":{}}" + "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} it "x.file without file invitation" $ - "{\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Nothing, fileInline = Nothing, fileDescr = Nothing} it "x.file.acpt" $ - "{\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" it "x.file.acpt.inv" $ - "{\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") Nothing "photo.jpg" it "x.file.cancel" $ - "{\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.file.cancel\",\"params\":{\"msgId\":\"AQIDBA==\"}}" #==# XFileCancel (SharedMsgId "\1\2\3\4") it "x.info" $ - "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XInfo testProfile it "x.info with empty full name" $ - "{\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.info\",\"params\":{\"profile\":{\"fullName\":\"\",\"displayName\":\"alice\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, contactLink = Nothing, preferences = testChatPreferences} it "x.contact with xContactId" $ - "{\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile (Just $ XContactId "\1\2\3\4") it "x.contact without XContactId" $ - "{\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XContact testProfile Nothing it "x.contact with content null" $ - "{\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing it "x.contact with content (ignored)" $ - "{\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing it "x.grp.inv" $ - "{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing} it "x.grp.inv with group link id" $ - "{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4"} it "x.grp.acpt without incognito profile" $ - "{\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpAcpt (MemberId "\1\2\3\4") it "x.grp.mem.new" $ - "{\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} + it "x.grp.mem.new with member chat version range" $ + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ - "{\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} + it "x.grp.mem.intro with member chat version range" $ + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.inv" $ - "{\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" - #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq} + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} + it "x.grp.mem.inv w/t directConnReq" $ + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.fwd" $ - "{\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" - #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = testConnReq} + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} + it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-2\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ - "{\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" #==# XGrpMemInfo (MemberId "\1\2\3\4") testProfile it "x.grp.mem.con" $ - "{\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.con\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpMemCon (MemberId "\1\2\3\4") it "x.grp.mem.con.all" $ - "{\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.con.all\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpMemConAll (MemberId "\1\2\3\4") it "x.grp.mem.del" $ - "{\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.del\",\"params\":{\"memberId\":\"AQIDBA==\"}}" #==# XGrpMemDel (MemberId "\1\2\3\4") it "x.grp.leave" $ - "{\"event\":\"x.grp.leave\",\"params\":{}}" + "{\"v\":\"1\",\"event\":\"x.grp.leave\",\"params\":{}}" ==# XGrpLeave it "x.grp.del" $ - "{\"event\":\"x.grp.del\",\"params\":{}}" + "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" ==# XGrpDel + it "x.grp.direct.inv" $ + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + #==# XGrpDirectInv testConnReq (Just $ MCText "hello") + it "x.grp.direct.inv without content" $ + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"https://simplex.chat/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%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + #==# XGrpDirectInv testConnReq Nothing it "x.info.probe" $ - "{\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.info.probe\",\"params\":{\"probe\":\"AQIDBA==\"}}" #==# XInfoProbe (Probe "\1\2\3\4") it "x.info.probe.check" $ - "{\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.info.probe.check\",\"params\":{\"probeHash\":\"AQIDBA==\"}}" #==# XInfoProbeCheck (ProbeHash "\1\2\3\4") it "x.info.probe.ok" $ - "{\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}" + "{\"v\":\"1\",\"event\":\"x.info.probe.ok\",\"params\":{\"probe\":\"AQIDBA==\"}}" #==# XInfoProbeOk (Probe "\1\2\3\4") it "x.ok" $ - "{\"event\":\"x.ok\",\"params\":{}}" + "{\"v\":\"1\",\"event\":\"x.ok\",\"params\":{}}" ==# XOk diff --git a/tests/SchemaDump.hs b/tests/SchemaDump.hs index cd493ab34e..f4538e4b3b 100644 --- a/tests/SchemaDump.hs +++ b/tests/SchemaDump.hs @@ -69,7 +69,9 @@ skipComparisonForDownMigrations = [ -- on down migration msg_delivery_events table moves down to the end of the file "20230504_recreate_msg_delivery_events_cleanup_messages", -- on down migration idx_chat_items_timed_delete_at index moves down to the end of the file - "20230529_indexes" + "20230529_indexes", + -- table and index definitions move down the file, so fields are re-created as not unique + "20230914_member_probes" ] getSchema :: FilePath -> FilePath -> IO String diff --git a/tests/Test.hs b/tests/Test.hs index 9010aefa0f..455d5459c7 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -1,5 +1,8 @@ +import Bots.BroadcastTests +import Bots.DirectoryTests import ChatClient import ChatTests +import ChatTests.Utils (xdescribe'') import Control.Logger.Simple import Data.Time.Clock.System import MarkdownTests @@ -15,14 +18,16 @@ main :: IO () main = do setLogLevel LogError -- LogDebug withGlobalLogging logCfg . hspec $ do + describe "Schema dump" schemaDumpTest describe "SimpleX chat markdown" markdownTests describe "SimpleX chat view" viewTests describe "SimpleX chat protocol" protocolTests describe "WebRTC encryption" webRTCTests - describe "Schema dump" schemaDumpTest around testBracket $ do describe "Mobile API Tests" mobileTests describe "SimpleX chat client" chatTests + xdescribe'' "SimpleX Broadcast bot" broadcastBotTests + xdescribe'' "SimpleX Directory service bot" directoryServiceTests where testBracket test = do t <- getSystemTime diff --git a/website/.eleventy.js b/website/.eleventy.js index 1592f5ab95..09fc7c2c44 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -52,7 +52,7 @@ const globalConfig = { } const translationsDirectoryPath = './langs' -const supportedRoutes = ["blog", "contact", "invitation", "docs", ""] +const supportedRoutes = ["blog", "contact", "invitation", "docs", "fdroid", ""] let supportedLangs = [] fs.readdir(translationsDirectoryPath, (err, files) => { if (err) { @@ -188,6 +188,50 @@ module.exports = function (ty) { return dom.serialize() }) + ty.addFilter('wrapH3s', function (content, page) { + if (!page.url.includes("/jobs/")) { + return content + } + + const dom = new JSDOM(content) + const document = dom.window.document + + const makeBlock = (block) => { + const jobTab = document.createElement('div') + jobTab.className = "job-tab" + + const flexDiv = document.createElement('div') + flexDiv.className = "flex items-center justify-between job-tab-btn cursor-pointer" + flexDiv.innerHTML = ` + <${block.tagName}>${block.innerHTML} + + + + ` + jobTab.appendChild(flexDiv) + + const jobContent = document.createElement('div') + jobContent.className = "job-tab-content" + jobTab.appendChild(jobContent) + + block.parentNode.insertBefore(jobTab, block) + block.remove() + + let sibling = jobTab.nextElementSibling + const siblingsToMove = [] + while (sibling && !['H3', 'H2'].includes(sibling.tagName)) { + siblingsToMove.push(sibling) + sibling = sibling.nextElementSibling + } + + siblingsToMove.forEach(el => jobContent.appendChild(el)) + } + + Array.from(document.querySelectorAll("h3")).forEach(makeBlock) + + return dom.serialize() + }) + ty.addShortcode("completeRoute", (obj) => { const urlParts = obj.url.split("/") @@ -238,6 +282,7 @@ module.exports = function (ty) { ty.addPassthroughCopy("src/video") ty.addPassthroughCopy("src/css") ty.addPassthroughCopy("src/js") + ty.addPassthroughCopy("src/lottie_file") ty.addPassthroughCopy("src/contact/*.js") ty.addPassthroughCopy("src/call") ty.addPassthroughCopy("src/hero-phone") @@ -270,7 +315,8 @@ module.exports = function (ty) { referenceMenu.data.forEach(referenceSubmenu => { docs.forEach(doc => { const url = doc.url.replace("/docs/", "") - const urlParts = url.split("/") + let urlParts = url.split("/") + urlParts = urlParts.filter((ele) => ele !== "") if (doc.inputPath.split('/').includes(referenceSubmenu)) { if (urlParts.length === 1 && urlParts[0] !== "") { @@ -348,7 +394,7 @@ module.exports = function (ty) { if (parsed.path.startsWith("../../blog")) { parsed.path = parsed.path.replace("../../blog", "/blog") } - parsed.path = parsed.path.replace(/\.md$/, ".html") + parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase() return uri.serialize(parsed) } }).use(markdownItAnchor, { diff --git a/website/customize_docs_frontmatter.js b/website/customize_docs_frontmatter.js index 8f2546e168..10031b5436 100644 --- a/website/customize_docs_frontmatter.js +++ b/website/customize_docs_frontmatter.js @@ -54,13 +54,17 @@ Object.entries(fileLanguageMapping).forEach(([fileName, languages]) => { // Calculate the permalink based on the file's location const linkPath = path.relative(directoryPath, fullPath).replace(/\.md$/, '.html'); const permalink = `/docs/${linkPath}`.toLowerCase(); - parsedMatter.data.permalink = permalink; + + if (fileName === 'JOIN_TEAM') { + parsedMatter.data.active_jobs = true; + } + if (!parsedMatter.data.permalink) parsedMatter.data.permalink = permalink; // Update the frontmatter with the new languages list parsedMatter.data.supportedLangsForDoc = languages; // Add the layout value - parsedMatter.data.layout = 'layouts/doc.html'; + if (!parsedMatter.data.layout) parsedMatter.data.layout = 'layouts/doc.html'; if (fullPath.startsWith(path.join(directoryPath, langFolder))) { // Non-English files diff --git a/website/langs/ar.json b/website/langs/ar.json index bb63289174..3fe698a3fe 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -99,7 +99,7 @@ "simplex-unique-2-overlay-1-title": "أفضل حماية من البريد العشوائي وإساءة الاستخدام", "hero-overlay-card-1-p-3": "أنت تحدد الخادم (الخوادم) المراد استخدامه لتلقي الرسائل وجهات الاتصال الخاصة بك — الخوادم التي تستخدمها لإرسال الرسائل إليهم. من المرجح أن تستخدم كل محادثة خادمين مختلفين.", "hero-overlay-card-1-p-1": "سأل العديد من المستخدمين: إذا لم يكن لدى SimpleX معرفات مستخدم، فكيف يمكنها معرفة مكان تسليم الرسائل؟ ", - "hero-overlay-card-1-p-2": "لتقديم الرسائل، بدلاً من معرفات المستخدم التي تستخدمها جميع المنصات الأخرى، يستخدم SimpleX معرفات مزدوجة مؤقتة مجهولة الهوية لقوائم انتظار الرسائل، مختلفة لكل اتصال من اتصالاتك — لا توجد معرفات مستخدم دائمة.", + "hero-overlay-card-1-p-2": "لتوصيل الرسائل، بدلاً من معرفات المستخدم التي تستخدمها جميع المنصات الأخرى، يستخدم SimpleX معرفات مزدوجة مؤقتة مجهولة الهوية لقوائم انتظار الرسائل، مختلفة لكل اتصال من اتصالاتك — لا توجد معرفات مستخدم دائمة.", "simplex-network-overlay-card-1-p-1": "بروتوكولات المراسلة والتطبيقات P2P بها مشاكل مختلفة تجعلها أقل موثوقية من SimpleX وأكثر تعقيدًا في التحليل و عرضة لعدة أنواع من الهجمات.", "hero-overlay-card-2-p-1": "عندما يكون لدى المستخدمين هويات ثابتة، حتى لو كان هذا مجرد رقم عشوائي، مثل معرف الجلسة، فهناك خطر يتمثل في أن الموفر أو المهاجم يمكنه مراقبة كيفية اتصال المستخدمين وعدد الرسائل التي يرسلونها.", "hero-overlay-card-1-p-6": "اقرأ المزيد في SimpleX whitepaper .", @@ -120,7 +120,7 @@ "simplex-unique-card-1-p-1": "يحمي 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-2-p-3": "SimpleX هو النظام الأساسي الأول الذي لا يحتوي على أي معرفات مستخدم صمّم ليكون خاصًا، وبهذه الطريقة تحمي مخطط اتصالاتك بشكل أفضل من أي بديل معروف.", "privacy-matters-overlay-card-3-p-2": "واحدة من أكثر القصص إثارة للصدمة هي تجربة محمدو ولد صلاحي الموصوفة في مذكراته والموضحة في فيلم موريتاني. تم وضعه في معتقل غوانتانامو بدون محاكمة، وتعرض للتعذيب هناك لمدة 15 عامًا بعد مكالمة هاتفية مع قريبه في أفغانستان، للاشتباه في تورطه في هجمات 11 سبتمبر، على الرغم من أنه عاش في ألمانيا طوال السنوات العشر الماضية.", "privacy-matters-overlay-card-3-p-3": "يتم القبض على الأشخاص العاديين بسبب ما يشاركونه عبر الإنترنت، حتى عبر حساباتهم \"المجهولة\"، وحتى في البلدان الديمقراطية.", "simplex-unique-overlay-card-1-p-1": "على عكس أنظمة المراسلة الأخرى، لا يحتوي SimpleX على معرفات مخصصة للمستخدمين. لا يعتمد على أرقام الهواتف أو العناوين المستندة إلى النطاقات (مثل البريد الإلكتروني أو XMPP)، أسماء المستخدمين، المفاتيح العامة أو حتى الأرقام العشوائية لتحديد مستخدميها — لا نعرف عدد الأشخاص الذين يستخدمون خوادم SimpleX الخاصة بنا.", @@ -132,7 +132,7 @@ "donate-here-to-help-us": "تبرّع هنا لمساعدتنا", "sign-up-to-receive-our-updates": "اشترك للحصول على آخر مستجداتنا", "enter-your-email-address": "أدخل عنوان بريدك الإلكتروني", - "get-simplex": "احصل على SimpleX", + "get-simplex": "احصل على SimpleX desktop app", "why-simplex-is": "لماذا SimpleX", "unique": "فريد من نوعه", "learn-more": "اقرأ أكثر", @@ -157,10 +157,10 @@ "comparison-section-list-point-7": "شبكات P2P إما لديها سلطة مركزية أو أن الشبكة كلها يمكن عرضة للخطر", "see-here": "اقرأ هنا", "no-secure": "لا - آمن", - "comparison-section-list-point-5": "لا يحمي المعلومات الوصفية للمستخدمين", + "comparison-section-list-point-5": "لا يحمي خصوصية البيانات الوصفية للمستخدمين", "comparison-section-list-point-6": "على الرغم من أن الـP2P موزعة، إلا أنها ليست فدرالية - يعملون كشبكة واحدة", "comparison-section-list-point-1": "عادة ما يكون مكوناً من رقم الهاتف، أو اسم المستخدم في بعض الأحيان", - "comparison-section-list-point-4": "إذا خوادم المشغّل مُخترقة", + "comparison-section-list-point-4": "إذا خوادم المشغّل مُخترقة. تحقق من رمز الأمان في Signal وبعض التطبيقات الأخرى للتخفيف منه", "simplex-unique-card-3-p-1": "يخزن SimpleX جميع بيانات المستخدم على الأجهزة العميلة بتنسيق قاعدة بيانات محمولة مشفرة — يمكن نقله إلى جهاز آخر.", "simplex-unique-card-4-p-1": "شبكة SimpleX لا مركزية بالكامل ومستقلة عن أي عملة مشفرة أو أي منصة أخرى، بخلاف الإنترنت.", "simplex-unique-card-4-p-2": "يمكنك استخدام SimpleX مع خوادمك الخاصة أو مع الخوادم التي نوفرها — ولا يزال الاتصال ممكن بأي مستخدم.", @@ -232,5 +232,16 @@ "guide": "الدليل", "docs-dropdown-2": "الوصول إلى ملفات اندرويد", "docs-dropdown-3": "الوصول إلى قاعدة بيانات الدردشة", - "glossary": "المعجم" + "glossary": "المعجم", + "docs-dropdown-8": "SimpleX خدمة الدليل", + "simplex-chat-via-f-droid": "SimpleX Chat عبر F-Droid", + "simplex-chat-repo": "مستودع SimpleX Chat", + "stable-and-beta-versions-built-by-developers": "الإصدارات الثابتة والتجريبية التي أنشأها المطورون", + "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-simplex-chat-repo-section-text": "لإضافته إلى عميل F-Droid، امسح رمز QR أو استخدم عنوان URL هذا:", + "f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يرجى تصدير قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.", + "comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تشفير بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق" } diff --git a/website/langs/bg.json b/website/langs/bg.json new file mode 100644 index 0000000000..8ab6d958ed --- /dev/null +++ b/website/langs/bg.json @@ -0,0 +1,3 @@ +{ + "developers": "Разработчици" +} diff --git a/website/langs/cs.json b/website/langs/cs.json index 99febbeacd..93aed0c9b2 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -114,7 +114,7 @@ "donate-here-to-help-us": "Přispějte zde a pomozte nám", "sign-up-to-receive-our-updates": "Přihlaste se k odběru novinek", "enter-your-email-address": "vložte svou e-mailovou adresu", - "get-simplex": "Získat SimpleX", + "get-simplex": "Získat SimpleX desktop app", "why-simplex-is": "Proč je SimpleX", "unique": "jedinečný", "learn-more": "Další informace", diff --git a/website/langs/de.json b/website/langs/de.json index c2c6fcf1e2..052b73235a 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -1,6 +1,6 @@ { - "simplex-privacy": "SimpleX Privatsphäre", - "simplex-network": "SimpleX Netzwerk", + "simplex-privacy": "Privatsphäre mit SimpleX", + "simplex-network": "SimpleX-Netzwerk", "home": "Startseite", "developers": "Entwickler", "reference": "Referenz", @@ -9,7 +9,7 @@ "features": "Funktionen", "simplex-private-card-6-point-2": "Um das zu verhindern, werden von SimpleX Einmal-Schlüssel Out-of-Band weitergeleitet, wenn Sie eine Adresse als Link oder QR-Code teilen.", "simplex-explained": "SimpleX erklärt", - "simplex-explained-tab-2-text": "2. Wie funktioniert es", + "simplex-explained-tab-2-text": "2. Wie es funktioniert", "simplex-explained-tab-3-text": "3. Was die Server sehen", "simplex-explained-tab-1-text": "1. Wie es die Nutzer erleben", "simplex-explained-tab-1-p-1": "Sie können Kontakte und Gruppen erstellen und haben Zwei-Wege-Kommunikation wie in jedem anderen Messenger.", @@ -127,7 +127,7 @@ "donate-here-to-help-us": "Spenden Sie, um uns zu unterstützen", "sign-up-to-receive-our-updates": "Melden Sie sich an, um Updates von uns zu erhalten", "enter-your-email-address": "Geben Sie Ihre Mail-Adresse ein", - "get-simplex": "Laden Sie sich SimpleX herunter", + "get-simplex": "Laden Sie sich SimpleX herunter desktop app", "learn-more": "Erfahren Sie mehr darüber", "more-info": "Weitere Informationen", "hide-info": "Informationen verbergen", @@ -170,7 +170,7 @@ "no-federated": "Nein - föderiert", "comparison-section-list-point-2": "DNS-basierte Adressen", "comparison-section-list-point-3": "Öffentlicher Schlüssel oder eine andere weltweit eindeutige ID", - "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden", + "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Securitycode überprüft werden, um dies zu entschärfen", "comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert - sie arbeiten als ein einziges Netzwerk", "comparison-section-list-point-7": "P2P-Netzwerke haben entweder eine zentrale Verwaltung oder das gesamte Netzwerk kann kompromittiert werden", "see-here": "Siehe hier", @@ -194,7 +194,7 @@ "comparison-section-list-point-1": "Normalerweise auf der Grundlage einer Telefonnummer, in einigen Fällen auf der Grundlage von Benutzernamen", "comparison-point-5-text": "Zentrale Komponente oder andere Netzwerk-weite Angriffe", "no-decentralized": "Nein - dezentralisiert", - "comparison-section-list-point-5": "Metadaten des Nutzers werden nicht geschützt", + "comparison-section-list-point-5": "Die Privatsphäre-Metadaten des Nutzers werden nicht geschützt", "simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von DHT, um Nachrichten zu routen. DHT-Designs müssen zwischen Zustellungsgarantie und Latenz ausgleichen. Verglichen mit P2P bietet SimpleX sowohl eine bessere Zustellungsgarantie, als auch eine niedrigere Latenz, weil eine Nachricht redundant und parallel über mehrere Server gesendet werden kann, wobei die durch den Empfänger ausgewählten Server genutzt werden. In P2P-Netzwerken werden Nachrichten sequentiell über O(log N) Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.", "simplex-unique-overlay-card-3-p-4": "Zwischen dem gesendeten und empfangenen Serververkehr gibt es keine gemeinsamen Kennungen oder Chiffriertexte — sodass ein Beobachter nicht ohne weiteres feststellen kann, wer mit wem kommuniziert, selbst wenn TLS kompromittiert wurde.", "simplex-unique-overlay-card-4-p-3": "Wenn Sie darüber nachdenken, für die SimpleX-Plattform entwickeln zu wollen, z.B. einen Chatbot für SimpleX-App-Nutzer oder die Integration der SimpleX-Chat-Bibliothek in Ihre mobilen Apps, kontaktieren Sie uns bitte für eine weitere Beratung und Unterstützung.", @@ -232,5 +232,16 @@ "back-to-top": "Zum Anfang", "guide-dropdown-3": "Geheime Gruppen", "docs-dropdown-7": "SimpleX Chat übersetzen", - "glossary": "Glossar" + "glossary": "Glossar", + "signing-key-fingerprint": "Fingerabdruck des Signaturschlüssels (SHA-256)", + "f-droid-org-repo": "F-Droid.org Repository", + "stable-versions-built-by-f-droid-org": "Von F-Droid.org erstellte stabile Versionen", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte die Chat-Datenbank exportieren und die App neu installieren.", + "releases-to-this-repo-are-done-1-2-days-later": "Die Versionen für dieses Repository werden 1..2 Tage später erstellt", + "docs-dropdown-8": "SimpleX Verzeichnisdienst", + "simplex-chat-via-f-droid": "SimpleX Chat per F-Droid", + "simplex-chat-repo": "SimpleX Chat Repository", + "stable-and-beta-versions-built-by-developers": "Von den Entwicklern erstellte stabile und Beta-Versionen", + "f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen scannen Sie den QR-Code oder nutzen Sie diese URL:", + "comparison-section-list-point-4a": "SimpleX-Relais können die E2E-Verschlüsselung nicht kompromittieren. Überprüfen Sie den Sicherheitscode, um einen möglichen Angriff auf den Out-of-Band-Kanal zu entschärfen" } diff --git a/website/langs/en.json b/website/langs/en.json index 74ea350e78..434aed8343 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -30,10 +30,12 @@ "hero-p-1": "Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
SimpleX does not, not even random numbers.
This radically improves your privacy.", "hero-overlay-1-textlink": "Why user IDs are bad for privacy?", "hero-overlay-2-textlink": "How does SimpleX work?", + "hero-overlay-3-textlink": "Security assessment", "hero-2-header": "Make a private connection", "hero-2-header-desc": "The video shows how you connect to your friend via their 1-time QR-code, in person or via a video link. You can also connect by sharing an invitation link.", "hero-overlay-1-title": "How does SimpleX work?", "hero-overlay-2-title": "Why user IDs are bad for privacy?", + "hero-overlay-3-title": "Security assessment", "feature-1-title": "E2E-encrypted messages with markdown and editing", "feature-2-title": "E2E-encrypted
images and files", "feature-3-title": "Decentralized secret groups —
only users know they exist", @@ -99,6 +101,9 @@ "hero-overlay-card-2-p-2": "They could then correlate this information with the existing public social networks, and determine some real identities.", "hero-overlay-card-2-p-3": "Even with the most private apps that use Tor v3 services, if you talk to two different contacts via the same profile they can prove that they are connected to the same person.", "hero-overlay-card-2-p-4": "SimpleX protects against these attacks by not having any user IDs in its design. And, if you use Incognito mode, you will have a different display name for each contact, avoiding any shared data between them.", + "hero-overlay-card-3-p-1": "Trail of Bits is a leading security and technology consultancy whose clients include big tech, governmental agencies and major blockchain projects.", + "hero-overlay-card-3-p-2": "Trail of Bits reviewed SimpleX platform cryptography and networking components in November 2022.", + "hero-overlay-card-3-p-3": "Read more in the announcement.", "simplex-network-overlay-card-1-p-1": "P2P messaging protocols and apps have various problems that make them less reliable than SimpleX, more complex to analyse, and vulnerable to several types of attack.", "simplex-network-overlay-card-1-li-1": "P2P networks rely on some variant of DHT to route messages. DHT designs have to balance delivery guarantee and latency. SimpleX has both better delivery guarantee and lower latency than P2P, because the message can be redundantly passed via several servers in parallel, using the servers chosen by the recipient. In P2P networks the message is passed through O(log N) nodes sequentially, using nodes chosen by the algorithm.", "simplex-network-overlay-card-1-li-2": "SimpleX design, unlike most P2P networks, has no global user identifiers of any kind, even temporary, and only uses temporary pairwise identifiers, providing better anonymity and metadata protection.", @@ -143,7 +148,7 @@ "donate-here-to-help-us": "Donate here to help us", "sign-up-to-receive-our-updates": "Sign up to receive our updates", "enter-your-email-address": "Enter your email address", - "get-simplex": "Get SimpleX", + "get-simplex": "Get SimpleX desktop app", "why-simplex-is": "Why SimpleX is", "unique": "unique", "learn-more": "Learn more", @@ -205,8 +210,9 @@ "comparison-section-list-point-1": "Usually based on a phone number, in some cases on usernames", "comparison-section-list-point-2": "DNS-based addresses", "comparison-section-list-point-3": "Public key or some other globally unique ID", - "comparison-section-list-point-4": "If operator’s servers are compromised", - "comparison-section-list-point-5": "Does not protect users' metadata", + "comparison-section-list-point-4a": "SimpleX relays cannot compromise e2e encryption. Verify security code to mitigate attack on out-of-band channel", + "comparison-section-list-point-4": "If operator’s servers are compromised. Verify security code in Signal and some other apps to mitigate it", + "comparison-section-list-point-5": "Does not protect users' metadata privacy", "comparison-section-list-point-6": "While P2P are distributed, they are not federated - they operate as a single network", "comparison-section-list-point-7": "P2P networks either have a central authority or the whole network can be compromised", "see-here": "see here", @@ -227,10 +233,22 @@ "docs-dropdown-5": "Host XFTP Server", "docs-dropdown-6": "WebRTC servers", "docs-dropdown-7": "Translate SimpleX Chat", + "docs-dropdown-8": "SimpleX Directory Service", + "docs-dropdown-9": "Downloads", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", "on-this-page": "On this page", "back-to-top": "Back to top", - "glossary": "Glossary" + "glossary": "Glossary", + "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", + "simplex-chat-repo": "SimpleX Chat repo", + "stable-and-beta-versions-built-by-developers": "Stable and beta versions built by the developers", + "f-droid-page-simplex-chat-repo-section-text": "To add it to your F-Droid client, scan the QR code or use this URL:", + "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", + "f-droid-org-repo": "F-Droid.org repo", + "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please export the chat database and re-install the app.", + "jobs": "Join team" } diff --git a/website/langs/es.json b/website/langs/es.json index 892531aedf..dfbb95d725 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -95,7 +95,7 @@ "simplex-unique-2-overlay-1-title": "La mejor protección contra el spam y el abuso", "simplex-unique-3-overlay-1-title": "Titularidad, control y seguridad de tus datos", "simplex-unique-4-overlay-1-title": "Totalmente descentralizado — los usuarios son dueños de la red SimpleX", - "hero-overlay-card-1-p-2": "Para entregar los mensajes, en lugar de los identificadores de usuario utilizados por todas las demás plataformas, SimpleX usa identificadores por pares anónimos y temporales de colas de mensajes, independientes para cada una de sus conexiones — no hay identificadores a largo plazo.", + "hero-overlay-card-1-p-2": "Para entregar los mensajes, en lugar de los identificadores de usuario utilizados por todas las demás plataformas, SimpleX emplea identificadores por pares temporales y anónimos de colas de mensajes, independientes para cada una de sus conexiones — no hay identificadores a largo plazo.", "hero-overlay-card-1-p-5": "Los perfiles de usuario, contactos y grupos sólo se almacenan en los dispositivos cliente; los mensajes se envían con cifrado de doble capa de extremo a extremo .", "hero-overlay-card-1-p-3": "Usted define qué servidor(es) se usan para recibir los mensajes; sus contactos — los servidores que se usan para enviar los mensajes. Es probable que cada conversación use dos servidores distintos.", "hero-overlay-card-2-p-4": "SimpleX protege frente estos ataques al no disponer de ID de usuario por diseño. Y si usa el modo incógnito, tendrá un nombre mostrado diferente por cada contacto, evitando cualquier dato compartido entre ellos.", @@ -137,7 +137,7 @@ "sign-up-to-receive-our-updates": "Suscríbase para recibir nuestras actualizaciones", "donate-here-to-help-us": "Para ayudarnos haga una donación aquí", "enter-your-email-address": "Escriba su dirección de correo electrónico", - "get-simplex": "Obtenga SimpleX", + "get-simplex": "Obtenga SimpleX desktop app", "why-simplex-is": "Por qué SimpleX es", "unique": "único", "learn-more": "Descubra más", @@ -179,8 +179,8 @@ "no-federated": "No - federado", "comparison-section-list-point-1": "Generalmente basado en un número de teléfono, en algunos casos en nombres de usuario", "comparison-section-list-point-2": "Direcciones basadas en DNS", - "comparison-section-list-point-4": "Si los servidores del operador se ven comprometidos", - "comparison-section-list-point-5": "No protege los metadatos del usuario", + "comparison-section-list-point-4": "Si los servidores del operador se ven comprometidos. Verifique el código de seguridad en Signal y alguna otra aplicación para mitigarlo", + "comparison-section-list-point-5": "No protege la privacidad de los metadatos del usuario", "comparison-section-list-point-3": "Clave pública o algun otro ID único a nivel global", "comparison-section-list-point-6": "A pesar de que las redes P2P son distribuidas, no son federadas - funcionan como una única red", "comparison-section-list-point-7": "Las redes P2P o bien tienen una autoridad central o toda la red puede verse comprometida", @@ -232,5 +232,16 @@ "menu": "Menú", "on-this-page": "En esta página", "back-to-top": "Volver arriba", - "glossary": "Glosario" + "glossary": "Glosario", + "stable-and-beta-versions-built-by-developers": "Versiones estables y beta compilados por los desarrolladores", + "f-droid-page-simplex-chat-repo-section-text": "Para añadirlo a tu cliente F-Droid escanea el código QR usa esta URL:", + "docs-dropdown-8": "Servicio Simplex Directory", + "simplex-chat-repo": "Repositorio Simplex Chat", + "simplex-chat-via-f-droid": "SimpleX Chat en F-Droid", + "f-droid-org-repo": "Repositorio F-Droid.org", + "stable-versions-built-by-f-droid-org": "Versión estable compilada por F-Droid.org", + "f-droid-page-f-droid-org-repo-section-text": "Los repositorios de SimpleX Chat y F-Droid.org firman con distinto certificado. Para cambiar, por favor exportar la base de datos y reinstala la aplicación.", + "signing-key-fingerprint": "Huella digital de la clave de firma (SHA-256)", + "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen 1-2 días más tarde en este repositorio", + "comparison-section-list-point-4a": "Los servidores de retransmisión no pueden comprometer la encriptación e2e. Para evitar posibles ataques, verifique el código de seguridad mediante un canal alternativo" } diff --git a/website/langs/fi.json b/website/langs/fi.json new file mode 100644 index 0000000000..963ed42c87 --- /dev/null +++ b/website/langs/fi.json @@ -0,0 +1,247 @@ +{ + "home": "Koti", + "developers": "Kehittäjät", + "reference": "Lisätietoja", + "blog": "Blogi", + "features": "Ominaisuudet", + "why-simplex": "Miksi SimpleX", + "simplex-privacy": "SimpleX yksityisyys", + "simplex-network": "SimpleX verkko", + "simplex-explained": "SimpleX selitetynä", + "simplex-explained-tab-1-text": "1. Mitä käyttäjät kokevat", + "simplex-explained-tab-2-text": "2. Miten se toimii", + "simplex-explained-tab-3-text": "3. Mitä palvelimet näkevät", + "simplex-explained-tab-1-p-2": "Kuinka se voi toimia yksisuuntaisten jonotusten kanssa ilman käyttäjäprofiilin tunnisteita?", + "simplex-explained-tab-2-p-1": "Jokaista yhteyttä varten käytetään kahta erillistä viestintäjonon mahdollistavaa palvelinta viestien lähettämiseen ja vastaanottamiseen.", + "simplex-explained-tab-2-p-2": "Palvelimet välittävät viestejä vain yhteen suuntaan eivätkä saa kokonaiskuvaa käyttäjän keskustelusta tai yhteyksistä.", + "simplex-explained-tab-3-p-2": "Käyttäjät voivat lisäksi parantaa metadata-yksityisyyttään käyttämällä Tor-verkkoa palvelimiin yhdistämiseen, mikä estää IP-osoitteen perusteella tapahtuvan yhteyksien tekemisen.", + "chat-bot-example": "Keskustelu­botti-esimerkki", + "smp-protocol": "SMP-protokolla", + "chat-protocol": "Keskustelu­protokolla", + "simplex-chat-protocol": "SimpleX Keskustelu­protokolla", + "terminal-cli": "Pääte CLI", + "terms-and-privacy-policy": "Käyttöehdot ja yksityisyys­käytäntö", + "hero-header": "Yksityisyys uudelleen määritelty", + "hero-subheader": "Ensimmäinen viestisovellus
ilman käyttäjätunnuksia", + "hero-overlay-1-textlink": "Miksi käyttäjätunnukset ovat huonoja yksityisyydelle?", + "hero-overlay-1-title": "Kuinka SimpleX toimii?", + "hero-overlay-2-title": "Miksi käyttäjätunnukset ovat huonoja yksityisyydelle?", + "feature-1-title": "Päästä päähän salattuja viestejä markdownin ja muokkaamisen kera", + "feature-2-title": "Päästä päähän salattuja
kuvia ja tiedostoja", + "feature-3-title": "Hajautetut salaiset ryhmät —
vain käyttäjät tietävät niiden olemassaolosta", + "feature-4-title": "Päästä päähän salattuja ääniviestejä", + "feature-5-title": "Katoavia viestejä", + "feature-8-title": "Incognito-tila —
ainutlaatuinen SimpleX Chatille", + "simplex-network-overlay-1-title": "Verrattuna P2P-viestintäprotokolliin", + "simplex-private-7-title": "Viestin eheys
vahvistus", + "simplex-private-9-title": "Yksisuuntaisia
viestijonoja", + "simplex-private-10-title": "Väliaikaiset nimettömät parittaiset tunnisteet", + "simplex-private-card-3-point-1": "Vain TLS 1.2/1.3 vahvoilla algoritmeilla käytetään asiakas-palvelin-yhteyksiin.", + "simplex-private-card-4-point-2": "Käyttääksesi SimpleX:ää Torin kautta, asenna Orbot-sovellus ja ota käyttöön SOCKS5-välityspalvelin (tai VPN iOS:lla).", + "simplex-private-card-7-point-1": "Viestien eheyden varmistamiseksi ne numeroituvat peräkkäin ja sisältävät edellisen viestin tiivisteen.", + "simplex-private-card-7-point-2": "Jos viestiä lisätään, poistetaan tai muutetaan, vastaanottaja saa ilmoituksen.", + "simplex-private-card-8-point-1": "SimpleX-palvelimet toimivat matalan viiveen sekoitussolmuina — saapuvilla ja lähtevillä viesteillä on erilainen järjestys.", + "simplex-private-card-10-point-1": "SimpleX käyttää väliaikaisia nimettömiä parittaisia osoitteita ja tunnistetietoja jokaiselle käyttäjäkontaktille tai ryhmän jäsenelle.", + "privacy-matters-2-title": "Vaalien manipulointi", + "privacy-matters-2-overlay-1-title": "Yksityisyys antaa sinulle valtaa", + "privacy-matters-2-overlay-1-linkText": "Yksityisyys antaa sinulle valtaa", + "privacy-matters-3-title": "Syyte viattomasta yhteydestä", + "privacy-matters-3-overlay-1-title": "Yksityisyys suojaa vapauttasi", + "simplex-unique-3-title": "Sinä hallitset tietojasi", + "hero-overlay-card-1-p-4": "Tämä ratkaisu estää kaikkien käyttäjien metadatan vuotamisen sovellustason tasolla. Lisätäksesi yksityisyyttä ja suojataksesi IP-osoitteesi, voit yhdistää viestintäpalvelimiin Tor-verkon kautta.", + "hero-overlay-card-1-p-5": "Vain asiakaslaitteet tallentavat käyttäjäprofiilit, yhteystiedot ja ryhmät; viestit lähetetään 2-kerroksisella päästä päähän salauksella.", + "hero-overlay-card-2-p-1": "Kun käyttäjillä on pysyvät tunnisteet, vaikka ne olisivat vain satunnaisia numeroita, kuten istunnon tunniste, on riski, että palveluntarjoaja tai hyökkääjä voi havaita miten käyttäjät ovat yhteydessä toisiinsa ja kuinka monta viestiä he lähettävät.", + "simplex-network-overlay-card-1-p-1": "P2P viestintäprotokollilla ja sovelluksilla on erilaisia ongelmia, jotka tekevät niistä vähemmän luotettavia kuin SimpleX, monimutkaisempia analysoida ja alttiita useille hyökkäystyypeille.", + "privacy-matters-overlay-card-1-p-1": "Monet suuret yritykset käyttävät tietoa siitä, keiden kanssa olet yhteydessä, arvioidakseen tulojasi, myydäkseen sinulle tarpeettomia tuotteita ja määrittääkseen hinnat.", + "privacy-matters-overlay-card-1-p-2": "Verkkokauppiaat tietävät, että alhaisempiin tuloluokkiin kuuluvat ihmiset todennäköisemmin tekevät kiireellisiä ostoksia, joten he voivat veloittaa korkeampia hintoja tai poistaa alennukset.", + "simplex-unique-overlay-card-3-p-1": "SimpleX Chat tallentaa kaikki käyttäjätiedot vain asiakaslaitteille käyttäen siirrettävää salattua tietokantamuotoa, joka voidaan viedä ja siirtää mihin tahansa tuettuun laitteeseen.", + "simplex-unique-overlay-card-3-p-2": "Päästä päähän salatut viestit säilytetään väliaikaisesti SimpleX-releay-palvelimilla, kunnes ne vastaanotetaan, minkä jälkeen ne poistetaan pysyvästi.", + "simplex-unique-card-1-p-1": "SimpleX suojaa profiilisi, yhteystietosi ja metatietosi yksityisyyden, piilottaen ne SimpleX-alustan palvelimilta ja kaikilta havainnoijilta.", + "simplex-unique-card-1-p-2": "Toisin kuin millään muulla olemassa olevalla viestintäalustalla, SimpleX:llä ei ole tunnisteita käyttäjille — ei edes satunnaisia numeroita.", + "simplex-unique-card-4-p-1": "SimpleX-verkko on täysin hajautettu ja riippumaton mistään kryptovaluutasta tai mistään muusta alustasta paitsi Internetistä.", + "simplex-unique-card-4-p-2": "Voit käyttää SimpleX:ää omien palvelimiesi kanssa tai meidän tarjoamillamme palvelimilla — ja silti yhdistyä mihin tahansa käyttäjään.", + "join": "Liity", + "hide-info": "Piilota tiedot", + "contact-hero-header": "Sait osoitteen yhdistämistä varten SimpleX Chatissa", + "invitation-hero-header": "Sait kertalinkini yhdistämistä varten SimpleX Chatissa", + "contact-hero-p-2": "Et ole vielä ladannut SimpleX Chat -sovellusta?", + "privacy-matters-section-subheader": "Metatietojesi yksityisyyden säilyttäminen — keneen puhut — suojaa sinua seuraavilta:", + "privacy-matters-section-label": "Varmista, ettei viestintäsovelluksesi pääse käsiksi tietoihisi!", + "simplex-private-section-header": "Mikä tekee SimpleX:stä yksityisen", + "simplex-network-1-header": "Toisin kuin P2P-verkot", + "simplex-network-1-overlay-linktext": "P2P-verkkojen ongelmia", + "protocol-1-text": "Signal, suuret alustat", + "protocol-2-text": "XMPP, Matrix", + "protocol-3-text": "P2P-protokollat", + "comparison-point-1-text": "Vaati globaalin identiteetin", + "comparison-point-2-text": "Mahdollisuus MITM-hyökkäykseen", + "comparison-point-3-text": "Riippuvuus DNS:stä", + "comparison-point-4-text": "Yksittäinen tai keskitetty verkko", + "yes": "Kyllä", + "no": "Ei", + "no-private": "Ei - yksityinen", + "no-secure": "Ei - turvallinen", + "no-resilient": "Ei - joustava", + "comparison-section-list-point-7": "P2P-verkoilla on joko keskitetty auktoriteetti tai koko verkko voidaan vaarantaa", + "see-here": "katso täältä", + "guide-dropdown-1": "Nopea aloitus", + "guide-dropdown-2": "Viestien lähettäminen", + "guide-dropdown-3": "Salaiset ryhmät", + "guide-dropdown-4": "Keskusteluprofiilit", + "guide-dropdown-7": "Yksityisyys ja turvallisuus", + "guide-dropdown-8": "Sovellusasetukset", + "guide-dropdown-9": "Yhteyksien luominen", + "docs-dropdown-5": "XFTP-palvelimen isännöiminen", + "docs-dropdown-6": "WebRTC-palvelimet", + "docs-dropdown-7": "Käännä SimpleX Chat", + "docs-dropdown-8": "SimpleX Hakupalvelu", + "on-this-page": "Tällä sivulla", + "back-to-top": "Takaisin ylös", + "glossary": "Sanasto", + "simplex-chat-repo": "SimpleX Chat -varasto", + "signing-key-fingerprint": "Allekirjoitusavaimen sormenjälki (SHA-256)", + "f-droid-org-repo": "F-Droid.org -varasto", + "stable-versions-built-by-f-droid-org": "Vakioversiot luotu F-Droid.org -varastoon", + "releases-to-this-repo-are-done-1-2-days-later": "Julkaisut tälle varastolle tehdään 1-2 päivää myöhemmin", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat ja F-Droid.org -varastot allekirjoittavat buildit eri avaimilla. Vaihtaaksesi, vienti keskustelutietokanta ja asenna sovellus uudelleen.", + "hero-overlay-2-textlink": "Kuinka SimpleX toimii?", + "hero-2-header": "Luo yksityinen yhteys", + "hero-2-header-desc": "Video näyttää, kuinka muodostat yhteyden ystävääsi heidän kertakäyttöiseen QR-koodiinsa, henkilökohtaisesti tai videolinkin kautta. Voit myös liittyä jakamalla kutsulinkin kautta.", + "feature-6-title": "Päästä päähän salattuja
puheluita ja videopuheluja", + "feature-7-title": "Siirrettävä salattu tietokanta — siirrä profiilisi toiselle laitteelle", + "simplex-explained-tab-1-p-1": "Voit luoda yhteyshenkilöitä ja ryhmiä sekä käydä kaksisuuntaisia keskusteluja kuten missä tahansa muussa viestisovelluksessa.", + "simplex-explained-tab-3-p-1": "Palvelimilla on erilliset anonyymit tunnistetiedot kullekin jonolle, eivätkä ne tiedä, mille käyttäjille ne kuuluvat.", + "donate": "Lahjoita", + "copyright-label": "© 2020-2023 SimpleX | Avoin projekti", + "hero-p-1": "Muissa sovelluksissa on käyttäjätunnuksia: Signal, Matrix, Session, Briar, Jami, Cwtch, jne.
SimpleX ei käytä niitä, ei edes satunnaisia numeroita.
Tämä parantaa yksityisyyttäsi radikaalisti.", + "simplex-private-1-title": "2 kerrosta päästä päähän salattua viestintää", + "simplex-private-2-title": "Lisäkerros palvelimen salaukselle", + "simplex-private-3-title": "Turvallinen tunnistettu
TLS-tiedonsiirto", + "simplex-private-4-title": "Vaihtoehtoinen pääsy Torin kautta", + "simplex-private-5-title": "Useita tasoja
sisältöpakkauksia", + "simplex-private-6-title": "Avainvaihto kanavan ulkopuolella
(out-of-band)", + "simplex-private-8-title": "Viestien sekoitus
korrelaation vähentämiseksi", + "simplex-private-card-1-point-1": "Kaksoisruuviprotokolla —
OTR-viestintä täydellisellä eteenpäinsalauksella ja murron palautuksella.", + "simplex-private-card-1-point-2": "NaCL-kryptolaatikko kussakin jonossa estämään liikenteen korrelaatiota viestijonojen välillä, jos TLS vaarantuu.", + "simplex-private-card-2-point-1": "Lisäkerros palvelimen salaukselle vastaanottajalle toimittaessa, jotta lähetetyn ja vastaanotetun palvelinliikenteen korrelaatio estyy, jos TLS vaarantuu.", + "simplex-private-card-3-point-2": "Palvelimen sormenjälki ja kanavan sitominen estävät välikäden hyökkäykset ja toistohyökkäykset.", + "simplex-private-card-3-point-3": "Yhteyden jatkaminen on estetty istuntohyökkäysten estämiseksi.", + "simplex-private-card-4-point-1": "Suojataksesi IP-osoitettasi, voit käyttää palvelimia Tor-verkon tai jonkin muun kuljetuskerroksen päällä.", + "simplex-private-card-5-point-1": "SimpleX käyttää sisältöpakkauksia jokaiselle salauskerrokselle estämään viestikoon hyökkäyksiä.", + "simplex-private-card-5-point-2": "Se saa erikokoiset viestit näyttämään samalta palvelimille ja verkon tarkkailijoille.", + "simplex-private-card-6-point-1": "Monet viestintäalustat ovat alttiita välikäden hyökkäyksille palvelimilta tai verkko-operaattoreilta.", + "simplex-private-card-6-point-2": "Estääkseen sen, SimpleX-sovellukset siirtävät yksittäiset avaimet kanavan ulkopuolella, kun jaat osoitteen linkkinä tai QR-koodina.", + "simplex-private-card-9-point-1": "Jokainen viestijono siirtää viestejä yhteen suuntaan, eri lähettävillä ja vastaanottavilla osoitteilla.", + "simplex-private-card-9-point-2": "Se vähentää hyökkäysvektoreita verrattuna perinteisiin viestivälittimiin ja saatavilla oleviin metatietoihin.", + "simplex-private-card-10-point-2": "Se mahdollistaa viestien toimittamisen ilman käyttäjäprofiilitunnisteita, tarjoten paremman metatietosuojan kuin vaihtoehdot.", + "privacy-matters-1-title": "Mainonnan ja hintasyrjinnän estäminen", + "privacy-matters-1-overlay-1-title": "Yksityisyys säästää rahaa", + "privacy-matters-1-overlay-1-linkText": "Yksityisyys säästää rahaa", + "privacy-matters-3-overlay-1-linkText": "Yksityisyys suojaa vapauttasi", + "simplex-unique-1-title": "Sinulla on täydellinen yksityisyys", + "simplex-unique-1-overlay-1-title": "Täysi yksityisyys identiteetistäsi, profiilistasi, kontakteistasi ja metatiedoista", + "simplex-unique-2-title": "Olet suojattu roskapostilta ja väärinkäytöksiltä", + "simplex-unique-2-overlay-1-title": "Paras suoja roskapostilta ja väärinkäytöksiltä", + "simplex-unique-3-overlay-1-title": "Omistusoikeus, hallinta ja tietojesi turvallisuus", + "simplex-unique-4-title": "Omistat SimpleX-verkon", + "simplex-unique-4-overlay-1-title": "Täysin hajautettu — käyttäjät omistavat SimpleX-verkon", + "hero-overlay-card-1-p-1": "Monet käyttäjät ovat kysyneet: jos SimpleX ei käytä käyttäjätunnisteita, miten se tietää minne viestit toimitetaan?", + "hero-overlay-card-1-p-3": "Määrität, minkä palvelimen(t) valitset viestien vastaanottamiseen, sekä kontaktisi — palvelimet, joita käytät viestien lähettämiseen heille. Jokainen keskustelu käyttää todennäköisesti kahta eri palvelinta.", + "hero-overlay-card-1-p-6": "Lue lisää SimpleX whitepaperista.", + "hero-overlay-card-1-p-2": "Viestien toimittamiseen SimpleX ei käytä muiden alustojen käyttäjätunnuksia, vaan sen sijaan se käyttää väliaikaisia nimettömiä parittaisia tunnisteita viestijonoille, jotka ovat erillisiä jokaiselle yhteydelle — pitkäaikaisia tunnisteita ei ole.", + "hero-overlay-card-2-p-3": "Jopa yksityisimmillä sovelluksilla, jotka käyttävät Tor v3 -palveluja, jos puhut kahdelle eri yhteyshenkilölle saman profiilin kautta, he voivat todistaa olevansa yhteydessä samaan henkilöön.", + "hero-overlay-card-2-p-4": "SimpleX suojaa näitä hyökkäyksiä vastaan, sillä siinä ei ole käyttäjätunnuksia toteutuksessaan. Ja jos käytät Incognito-tilaa, sinulla on eri näyttönimi jokaiselle yhteyshenkilölle, mikä estää yhteisten tietojen jakamisen heidän välillään.", + "hero-overlay-card-2-p-2": "Tämän tiedon avulla he voisivat yhdistää sen olemassa oleviin julkisiin sosiaalisiin verkostoihin ja määrittää joitakin todellisia identiteettejä.", + "simplex-network-overlay-card-1-li-1": "P2P-verkot luottavat jonkinlaiseen DHT-varianttiin viestien reitittämiseksi. DHT-suunnittelun on tasapainotettava toimituksen varmuutta ja latenssia. SimpleX:llä on parempi toimitusvarmuus ja pienempi latenssi kuin P2P-verkoilla, koska viesti voidaan toimittaa redundanssina useiden palvelimien kautta samanaikaisesti, käyttäen vastaanottajan valitsemia palvelimia. P2P-verkoissa viesti kulkee läpi O(log N) solmun sekvenssissä, käyttäen algoritmin valitsemia solmuja.", + "simplex-network-overlay-card-1-li-2": "SimpleX-toteutus, toisin kuin useimmat P2P-verkot, ei käytä globaaleja käyttäjätunnisteita millään tavalla, ei edes tilapäisiä, ja käyttää ainoastaan tilapäisiä parillisia tunnisteita, tarjoten paremman anonymiteetin ja metadatansuojan.", + "simplex-network-overlay-card-1-li-6": "P2P-verkot saattavat olla alttiita DRDoS-hyökkäyksille, kun asiakkaat voivat lähettää uudelleen ja voimistaa liikennettä, mikä johtaa verkko-laajuiseen palvelunestohyökkäykseen. SimpleX-asiakkaat välittävät liikennettä vain tunnetuilta yhteyksiltä eivätkä voi olla käytettävissä hyökkääjänä liikenteen voimistamiseen koko verkossa.", + "simplex-network-overlay-card-1-li-3": "P2P ei ratkaise välikäden hyökkäys -ongelmaa, ja useimmat olemassa olevat toteutukset eivät käytä kanavan ulkopuolisia viestejä alkuperäiseen avaimenvaihtoon. SimpleX käyttää kanavan ulkopuolisia viestejä tai, joissakin tapauksissa, jo valmiiksi turvallisia ja luotettuja yhteyksiä alkuperäiseen avaimenvaihtoon.", + "simplex-network-overlay-card-1-li-4": "P2P-toteutukset voivat estyä joidenkin Internet-palveluntarjoajien (kuten BitTorrent) toimesta. SimpleX on kuljetusprotokollasta riippumaton - se voi toimia yleisten verkkoprotokollien kautta, kuten esimerkiksi WebSockets.", + "simplex-network-overlay-card-1-li-5": "Kaikki tunnetut P2P-verkot saattavat olla alttiita Sybil-hyökkäykselle, koska jokainen solmu on löydettävissä, ja verkko toimii kokonaisuutena. Tunnetut keinot sen lieventämiseksi vaativat joko keskitetyn komponentin tai kalliin työn todistuksen. SimpleX-verkolla ei ole palvelimen löydettävyyttä, se on fragmentoitunut ja toimii useina eristettyinä aliverkkoina, mikä tekee verkko-laajuisista hyökkäyksistä mahdottomia.", + "privacy-matters-overlay-card-1-p-3": "Jotkut rahoitus- ja vakuutusyhtiöt käyttävät sosiaalisia verkostoja määrittääkseen korkoja ja vakuutusmaksuja. Se tekee usein alhaisempiin tuloihin kuuluvien ihmisten maksavan enemmän — sitä kutsutaan 'köyhyyslisäksi'.", + "privacy-matters-overlay-card-1-p-4": "SimpleX-alusta suojaa yhteyksiesi yksityisyyttä paremmin kuin mikään vaihtoehto, estäen täysin yhteysverkkosi tulemisen saataville mille tahansa yrityksille tai organisaatioille. Vaikka ihmiset käyttävät SimpleX Chatin tarjoamia palvelimia, emme tiedä käyttäjien määrää tai heidän yhteyksiään.", + "privacy-matters-overlay-card-2-p-3": "SimpleX on ensimmäinen alusta, jolla ei ole mitään käyttäjätunnisteita suunnittelussaan, suojaten siten yhteyksesi verkkoa paremmin kuin mikään tunnettu vaihtoehto.", + "privacy-matters-overlay-card-2-p-1": "Ei niin kauan sitten huomasimme merkittävien vaalien olevan manipuloitavissa kunnioitetun konsulttiyrityksen toimesta, joka käytti sosiaalisia verkostoja vääristämään käsitystämme todellisesta maailmasta ja manipuloimaan ääniämme.", + "privacy-matters-overlay-card-3-p-1": "Kaikkien pitäisi välittää viestinnän yksityisyydestä ja turvallisuudesta — harmittomat keskustelut voivat asettaa sinut vaaraan, vaikka sinulla ei olisi mitään piilotettavaa.", + "privacy-matters-overlay-card-2-p-2": "Ollaksesi objektiivinen ja tehdäksesi itsenäisiä päätöksiä, sinun on hallittava tietotilaasi. Se on mahdollista vain, jos käytät yksityistä viestintäalustaa, jolla ei ole pääsyä sosiaaliseen verkostoosi.", + "privacy-matters-overlay-card-3-p-2": "Yksi järkyttävimmistä tarinoista on Mohamedou Ould Salahi'n kokemus, joka on kuvattu hänen muistelmissaan ja esitetty The Mauritanian -elokuvassa. Hänet laitettiin Guantanamo-leirille ilman oikeudenkäyntiä ja häntä kidutettiin siellä 15 vuotta puhelun jälkeen sukulaiselleen Afganistanissa, epäiltynä osallisuudesta 9/11-iskuihin, vaikka hän oli asunut Saksassa edelliset 10 vuotta.", + "privacy-matters-overlay-card-3-p-3": "Tavalliset ihmiset pidätetään siitä, mitä he jakavat verkossa, jopa 'anonyymien' tiliensä kautta, jopa demokraattisissa maissa.", + "privacy-matters-overlay-card-3-p-4": "Ei riitä, että käytät päästä päähän salattua viestintäsovellusta, meidän kaikkien pitäisi käyttää viestintäsovelluksia, jotka suojelevat henkilökohtaisten verkostojemme yksityisyyttä — keiden kanssa olemme yhteydessä.", + "simplex-unique-overlay-card-1-p-1": "Toisin kuin muut viestintäalustat, SimpleX:llä ei ole mitään tunnisteita käyttäjille. Se ei luota puhelinnumeroihin, verkkotunnuksiin perustuviin osoitteisiin (kuten sähköposti tai XMPP), käyttäjänimiin, julkisiin avaimiin tai edes satunnaisiin numeroihin tunnistaakseen käyttäjänsä — emme tiedä kuinka monta ihmistä käyttää SimpleX-palvelimiamme.", + "simplex-unique-overlay-card-1-p-2": "Viestien toimittamiseksi SimpleX käyttää parittaisia nimettömiä osoitteita kaksisuuntaisille viestijonoille, jotka ovat erilliset vastaanotetuille ja lähetetyille viesteille, yleensä eri palvelimien kautta. SimpleX:n käyttö on kuin eri “kertakäyttöinen” sähköposti tai puhelin jokaiselle yhteydelle, eikä sinun tarvitse vaivautua niiden hallitsemiseen.", + "simplex-unique-overlay-card-1-p-3": "Tämä suunnittelu suojaa sitä, kenen kanssa kommunikoit, piilottamalla sen SimpleX-alustan palvelimilta ja kaikilta havainnoijilta. Piilottaaksesi IP-osoitteesi palvelimilta, voit yhdistää SimpleX-palvelimiin Tor-verkon kautta.", + "simplex-unique-overlay-card-2-p-1": "Koska sinulla ei ole tunnistetta SimpleX-alustalla, kukaan ei voi ottaa sinuun yhteyttä, ellei jaa kertakäyttöistä tai väliaikaista käyttäjäosoitetta, kuten QR-koodia tai linkkiä.", + "simplex-unique-overlay-card-2-p-2": "Jopa valinnaisen käyttäjäosoitteen kanssa, vaikka sitä voitaisiin käyttää roskapostiyhteyspyyntöjen lähettämiseen, voit vaihtaa sen tai poistaa sen kokonaan menettämättä mitään yhteyksiäsi.", + "simplex-unique-overlay-card-3-p-3": "Toisin kuin liitettyjen verkkojen palvelimet (sähköposti, XMPP tai Matrix), SimpleX-palvelimet eivät tallenna käyttäjätilejä, ne vain välittävät viestejä, suojaten molempien osapuolien yksityisyyttä.", + "simplex-unique-overlay-card-3-p-4": "Lähetetyssä ja vastaanotetussa palvelimen liikenteessä ei ole yhteisiä tunnisteita tai salaustekstiä — jos joku havaitsee sen, he eivät voi helposti selvittää kuka kommunikoi kenen kanssa, vaikka TLS olisi vaarantunut.", + "simplex-unique-overlay-card-4-p-3": "Jos harkitset kehittämistä SimpleX-alustalle, esimerkiksi chat-botin luomista SimpleX-sovelluksen käyttäjille tai SimpleX Chat -kirjaston integrointia mobiilisovelluksiisi, ole hyvä ja ota yhteyttä saadaksesi neuvoja ja tukea.", + "simplex-unique-overlay-card-4-p-1": "Voit käyttää SimpleX:ää omien palvelimiesi kanssa ja silti kommunikoida ihmisten kanssa, jotka käyttävät meille tarjottuja valmiiksi määritettyjä palvelimia.", + "simplex-unique-overlay-card-4-p-2": "SimpleX-alusta käyttää avoimeen protokollaan ja tarjoaa SDK:n chatbotien luomiseen, mahdollistaen palvelujen toteuttamisen, joiden kanssa käyttäjät voivat olla vuorovaikutuksessa SimpleX Chat -sovellusten kautta — odotamme innolla nähdä, millaisia SimpleX-palveluja voit rakentaa.", + "simplex-unique-card-3-p-1": "SimpleX tallentaa kaikki käyttäjätiedot asiakaslaitteille siirrettävässä salatussa tietokannan muodossa — se voidaan siirtää toiseen laitteeseen.", + "simplex-unique-card-3-p-2": "Päästä päähän salatut viestit säilytetään väliaikaisesti SimpleX-releay-palvelimilla, kunnes ne vastaanotetaan, minkä jälkeen ne poistetaan pysyvästi.", + "we-invite-you-to-join-the-conversation": "Kutsumme sinut mukaan keskusteluun", + "join-the-REDDIT-community": "Liity REDDIT-yhteisöön", + "simplex-unique-card-2-p-1": "Koska sinulla ei ole tunnistetta tai kiinteää osoitetta SimpleX-alustalla, kukaan ei voi ottaa sinuun yhteyttä, ellet jaa kertakäyttöistä tai väliaikaista käyttäjäosoitetta, kuten QR-koodia tai linkkiä.", + "join-us-on-GitHub": "Liity GitHubissa", + "donate-here-to-help-us": "Tue meitä täällä lahjoituksilla", + "sign-up-to-receive-our-updates": "Tilaa päivityksemme", + "enter-your-email-address": "Syötä sähköpostiosoitteesi", + "get-simplex": "Hanki SimpleX desktop app", + "why-simplex-is": "Miksi SimpleX on", + "unique": "ainutlaatuinen", + "learn-more": "Lue lisää", + "more-info": "Lisätietoja", + "contact-hero-subheader": "Skannaa QR-koodi SimpleX Chat -sovelluksella puhelimessasi tai tabletissasi.", + "contact-hero-p-1": "Tämän linkin julkiset avaimet ja viestijonon osoite eivät lähetä verkkoa pitkin, kun katsot tätä sivua — ne sisältyvät linkin URL:n hash-osaan.", + "contact-hero-p-3": "Käytä alla olevia linkkejä ladataksesi sovelluksen.", + "scan-qr-code-from-mobile-app": "Skannaa QR-koodi mobiilisovelluksesta", + "to-make-a-connection": "Yhteyden muodostaminen:", + "install-simplex-app": "Asenna SimpleX-sovellus", + "connect-in-app": "Yhdisty sovelluksessa", + "open-simplex-app": "Avaa Simplex-sovellus", + "tap-the-connect-button-in-the-app": "Napauta sovelluksessa olevaa ‘yhdistä’-painiketta", + "scan-the-qr-code-with-the-simplex-chat-app": "Skannaa QR-koodi SimpleX Chat -sovelluksella", + "scan-the-qr-code-with-the-simplex-chat-app-description": "Tämän linkin julkiset avaimet ja viestijonon osoite eivät lähetä verkkoa pitkin, kun katsot tätä sivua —
ne sisältyvät linkin URL:n hash-osaan.", + "installing-simplex-chat-to-terminal": "Asentaminen SimpleX Chat terminaaliin", + "see-simplex-chat": "Katso SimpleX Chat", + "use-this-command": "Käytä tätä komentoa:", + "github-repository": "GitHub-varasto", + "the-instructions--source-code": "ohjeet, miten ladata tai kääntää se lähdekoodista.", + "if-you-already-installed-simplex-chat-for-the-terminal": "Jos olet jo asentanut SimpleX Chatin terminaaliin", + "if-you-already-installed": "Jos olet jo asentanut", + "simplex-chat-for-the-terminal": "SimpleX Chatin terminaaliin", + "copy-the-command-below-text": "kopioi alla oleva komento ja käytä sitä keskustelussa:", + "privacy-matters-section-header": "Miksi yksityisyys on tärkeää", + "tap-to-close": "Napauta sulkeaksesi", + "simplex-network-section-header": "SimpleX Verkko", + "simplex-network-section-desc": "Simplex Chat tarjoaa parhaan yksityisyyden yhdistämällä P2P-verkkojen ja liitettävien verkkojen edut.", + "simplex-network-1-desc": "Kaikki viestit lähetetään palvelimien kautta, mikä sekä parantaa metatietojen yksityisyyttä että mahdollistaa luotettavan asynkronisen viestien toimituksen, samalla välttäen monia", + "simplex-network-2-header": "Toisin kuin liitettävät verkot", + "simplex-network-3-desc": "palvelimet tarjoavat yksisuuntaisia jonopalveluja yhdistääkseen käyttäjät, mutta niillä ei ole näkyvyyttä verkon yhteyskarttaan — ainoastaan käyttäjillä on.", + "comparison-section-header": "Vertailu muihin protokolliin", + "simplex-network-2-desc": "SimpleXin relea-palvelimet EIVÄT tallenna käyttäjäprofiileja, yhteystietoja ja toimitettuja viestejä, ne EIVÄT yhdisty toisiinsa, eikä ole OLE olemassa palvelinluetteloa.", + "simplex-network-3-header": "SimpleX-verkko", + "comparison-point-5-text": "Keskuskomponentti tai muu verkkoa koskeva hyökkäys", + "no-decentralized": "Ei - hajautettu", + "no-federated": "Ei - liitetty", + "comparison-section-list-point-1": "Yleensä pohjautuu puhelinnumeroon, joissain tapauksissa käyttäjänimiin", + "comparison-section-list-point-2": "DNS-pohjaiset osoitteet", + "comparison-section-list-point-3": "Julkinen avain tai jokin muu maailmanlaajuisesti uniikki tunniste", + "comparison-section-list-point-4a": "SimpleXin releapalvelimet eivät voi vaarantaa päästä päähän -salausta. Tarkista turvakoodi hyödyntääksesi hyökkäyssuojaa ylitys-kanavalla", + "comparison-section-list-point-5": "Ei suojaa käyttäjien metatietojen yksityisyyttä", + "comparison-section-list-point-6": "Vaikka P2P-verkot ovat hajautettuja, ne eivät ole liitettäviä - ne toimivat yhtenä verkostona", + "guide-dropdown-5": "Datan hallinta", + "guide-dropdown-6": "Ääni- ja videopuhelut", + "comparison-section-list-point-4": "Jos operaattorin palvelimet ovat vaarantuneet. Tarkista turvakoodi Signalissa ja joissakin muissa sovelluksissa suojautuaksesi hyökkäykseltä", + "guide": "Opas", + "docs-dropdown-1": "SimpleX-alusta", + "docs-dropdown-2": "Android-tiedostoihin pääseminen", + "docs-dropdown-3": "Keskustelutietokantaan pääseminen", + "docs-dropdown-4": "SMP-palvelimen isännöiminen", + "newer-version-of-eng-msg": "Tästä sivusta on uudempi versio englanniksi.", + "click-to-see": "Näytä napsauttamalla", + "menu": "Valikko", + "simplex-chat-via-f-droid": "SimpleX Chat F-Droidin kautta", + "stable-and-beta-versions-built-by-developers": "Kehittäjien luomat vakaat ja beta-versiot", + "f-droid-page-simplex-chat-repo-section-text": "Lisätäksesi sen F-Droid-asiakkaaseesi, skannaa QR-koodi tai käytä tätä URL-osoitetta:" +} diff --git a/website/langs/fr.json b/website/langs/fr.json index b38e56c481..37dc6d5a14 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -143,7 +143,7 @@ "donate-here-to-help-us": "Faites un don ici pour nous aider", "sign-up-to-receive-our-updates": "Inscrivez-vous pour recevoir nos mises à jour", "enter-your-email-address": "Entrez votre adresse e-mail", - "get-simplex": "Obtenir SimpleX", + "get-simplex": "Obtenir SimpleX desktop app", "why-simplex-is": "Pourquoi SimpleX est", "unique": "unique", "learn-more": "En savoir plus", @@ -205,8 +205,8 @@ "comparison-section-list-point-1": "Généralement basé sur un numéro de téléphone, dans certains cas sur des noms d'utilisateur", "comparison-section-list-point-2": "Adresses basées sur le DNS", "comparison-section-list-point-3": "Clé publique ou tout autre identifiant global unique", - "comparison-section-list-point-4": "Si les serveurs de l'opérateur sont compromis", - "comparison-section-list-point-5": "Ne protège pas les métadonnées des utilisateurs", + "comparison-section-list-point-4": "Si les serveurs de l'opérateur sont compromis. Vérifier les codes de sécurités sur Signal et d'autres applications pour limiter les risques", + "comparison-section-list-point-5": "Ne protège pas la confidentialité des métadonnées des utilisateurs", "comparison-section-list-point-6": "Bien que les P2P soient distribués, ils ne sont pas fédérés - ils fonctionnent comme un seul réseau", "comparison-section-list-point-7": "Les réseaux P2P ont soit une autorité centrale, soit l'ensemble du réseau peut être compromis", "voir-ici": "voir ici", @@ -233,5 +233,16 @@ "guide": "Guide", "docs-dropdown-4": "Hébergement d'un serveur SMP", "on-this-page": "Sur cette page", - "glossary": "Glossaire" + "glossary": "Glossaire", + "releases-to-this-repo-are-done-1-2-days-later": "Les mises à jour de ce dépôt sont faites 1 à 2 jours plus tard", + "f-droid-page-f-droid-org-repo-section-text": "Les dépôts SimpleX Chat et F-Droid.org signent les builds avec des clés différentes. Pour changer, veuillez exporter la base de données des chats et réinstaller l'application.", + "docs-dropdown-8": "Service de répertoire SimpleX", + "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", + "simplex-chat-repo": "Dépot SimpleX Chat", + "stable-and-beta-versions-built-by-developers": "Versions stables et bêta crées par les développeurs", + "f-droid-page-simplex-chat-repo-section-text": "Pour l'ajouter à votre client F-Droid scannez le code QR ou utilisez cette URL :", + "signing-key-fingerprint": "Empreinte de signature numérique (SHA-256)", + "f-droid-org-repo": "Dépot F-Droid.org", + "stable-versions-built-by-f-droid-org": "Versions stables créées par F-Droid.org", + "comparison-section-list-point-4a": "Les relais SimpleX ne peuvent pas compromettre le chiffrement e2e. Vérifier le code de sécurité pour limiter les attaques sur le canal hors bande" } diff --git a/website/langs/he.json b/website/langs/he.json new file mode 100644 index 0000000000..c814e405c0 --- /dev/null +++ b/website/langs/he.json @@ -0,0 +1,78 @@ +{ + "home": "מסך הבית", + "developers": "מפתחים", + "reference": "הפניה", + "blog": "בלוג", + "features": "מאפיינים", + "why-simplex": "למה SimpleX", + "simplex-privacy": "פרטיות SimpleX", + "simplex-network": "רשת SimpleX", + "simplex-explained": "תיאור SimpleX", + "simplex-explained-tab-1-text": "1. חוויית משתמש", + "simplex-explained-tab-2-text": "2. איך זה עובד", + "simplex-chat-protocol": "פרוטוקול SimpleX Chat", + "terminal-cli": "ממשק שורת פקודה", + "terms-and-privacy-policy": "תנאים ומדיניות פרטיות", + "hero-header": "פרטיות מוגדרת מחדש", + "hero-subheader": "מערכת העברת ההודעות הראשונה
ללא מזהי שתמש", + "hero-overlay-1-textlink": "מדוע מזהי משתמש מזיקים לפרטיות?", + "hero-overlay-2-textlink": "איך SimpleX עובד?", + "hero-2-header": "יצירת חיבור פרטי", + "hero-2-header-desc": "הסרטון מראה כיצד אתם יוצרים קשר עם חברכם באמצעות קוד QR חד פעמי, באופן אישי או באמצעות קישור וידאו. באפשרותכם גם להתחבר על-ידי שיתוף קישור ההזמנה.", + "hero-overlay-1-title": "איך SimpleX עובד?", + "feature-1-title": "הודעות מוצפנות מקצה לקצה עם סימונים ואפשרויות עריכה", + "feature-2-title": "תמונות וקבצים
מוצפנים מקצה לקצה", + "feature-3-title": "קבוצות סודיות מבוזרות —
רק המשתמשים יודעים שהן קיימות", + "simplex-private-3-title": "תעבורת TLS
מאובטחת ומאומתת", + "simplex-private-card-1-point-2": "תיבת הצפנה NaCL בכל תור כדי למנוע קורלציית תעבורה בין תורי הודעות במקרה שאבטחת TLS נפגעה.", + "simplex-private-6-title": "החלפת מפתחות
מחוץ לרשת", + "simplex-private-7-title": "בדיקת תקינות
ההודעה", + "simplex-private-8-title": "ערבוב הודעות
לשם הפחתת קורלציה", + "simplex-private-9-title": "תורי הודעות
חד-כיווניים", + "simplex-private-10-title": "מזהים זמניים אנונימיים בזוגות", + "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 attack).", + "simplex-private-card-5-point-1": "SimpleX משתמש בריפוד תוכן עבור כל שכבת הצפנה כדי לסכל התקפות בגודל הודעה.", + "simplex-private-card-5-point-2": "זה גורם להודעות בגדלים שונים להיראות זהים לשרתים ולמשקיפים ברשת.", + "simplex-private-card-6-point-1": "פלטפורמות תקשורת רבות חשופות להתקפות אדם בתווך (MITM) על ידי שרתים או ספקי רשת.", + "simplex-private-card-9-point-2": "זה מפחית את וקטורי ההתקפה, בהשוואה למתווכי הודעות מסורתיים, ואת המטא-נתונים הזמינים.", + "simplex-private-card-10-point-2": "זה מאפשר להעביר הודעות ללא מזהי פרופיל משתמש, ומספק פרטיות מטא-נתונים טובה יותר מאשר חלופות אחרות.", + "privacy-matters-1-title": "פרסום ואפליית מחירים", + "privacy-matters-1-overlay-1-linkText": "פרטיות חוסכת לכם כסף", + "privacy-matters-2-title": "מניפולציה בבחירות", + "privacy-matters-2-overlay-1-title": "פרטיות מעניקה לכם עוצמה", + "privacy-matters-3-overlay-1-title": "פרטיות מגנה על החופש שלכם", + "simplex-explained-tab-3-text": "3. מה השרתים רואים", + "simplex-explained-tab-2-p-1": "עבור כל חיבור, שני תורי העברת הודעות נפרדים משמשים לשליחה וקבלה של הודעות דרך שרתים שונים.", + "simplex-explained-tab-2-p-2": "שרתים מעבירים הודעות רק בכיוון אחד, מבלי לקבל את התמונה המלאה של השיחה או החיבורים של המשתמש.", + "simplex-explained-tab-3-p-1": "לשרתים יש אישורים אנונימיים נפרדים לכל תור, ואינם יודעים לאילו משתמשים הם שייכים.", + "simplex-explained-tab-1-p-2": "איך זה יכול לעבוד עם תורים חד-כיווניים וללא מזהי פרופיל משתמש?", + "simplex-explained-tab-3-p-2": "משתמשים יכולים לשפר עוד יותר את פרטיות המטא-נתונים על ידי שימוש ב- Tor כדי לגשת לשרתים, ולמנוע קורלציה לפי כתובת IP.", + "chat-bot-example": "דוגמה לצ'אט בוט", + "smp-protocol": "פרוטוקול SMP", + "chat-protocol": "פרוטוקול צ'אט", + "donate": "תרומה", + "copyright-label": "© 2020-2023 SimpleX | פרויקט קוד פתוח", + "hero-p-1": "לאפליקציות אחרות יש מזהי משתמש: Signal, Matrix, Session, Briar, Jami, Cwtch וכו'.
ל-SimpleX אין, אפילו לא מספרים אקראיים.
זה משפר באופן קיצוני את הפרטיות שלך.", + "hero-overlay-2-title": "מדוע מזהי משתמש מזיקים לפרטיות?", + "feature-6-title": "שיחות שמע ווידאו
מוצפנות מקצה לקצה", + "feature-4-title": "הודעות קוליות מוצפנות מקצה לקצה", + "feature-5-title": "הודעות נעלמות", + "feature-7-title": "מסד נתונים מוצפן נייד — העברת הפרופיל שלכם למכשיר אחר", + "feature-8-title": "מצב זהות נסתרת —
ייחודי ל-SimpleX Chat", + "simplex-private-4-title": "אופציונלי
גישה דרך Tor", + "simplex-network-overlay-1-title": "השוואה לפרוטוקולי העברת הודעות P2P", + "simplex-private-2-title": "שכבה נוספת של
הצפנת שרת", + "simplex-private-1-title": "2 שכבות של
הצפנה מקצה לקצה", + "simplex-private-5-title": "שכבות מרובות של
ריפוד תוכן", + "simplex-private-card-3-point-3": "חידוש החיבור מושבת כדי למנוע התקפות הפעלה.", + "simplex-private-card-4-point-1": "כדי להגן על כתובת ה-IP שלכם, אתם יכולים לגשת לשרתים דרך Tor או רשת שכבת-על אחרת של תעבורה.", + "simplex-private-card-4-point-2": "כדי להשתמש ב-SimpleX דרך Tor, התקן את אפליקציית Orbot והפעל את SOCKS5 proxy (או VPN ב-iOS).", + "simplex-private-card-7-point-2": "אם הודעה כלשהי תתווסף, תוסר או תשתנה, הנמען יקבל התראה.", + "simplex-private-card-9-point-1": "כל תור הודעות מעביר הודעות בכיוון אחד, עם כתובות השליחה והקבלה השונות.", + "privacy-matters-1-overlay-1-title": "פרטיות חוסכת לכם כסף", + "privacy-matters-2-overlay-1-linkText": "פרטיות מעניקה לכם עוצמה", + "privacy-matters-3-overlay-1-linkText": "פרטיות מגנה על החופש שלכם", + "simplex-explained-tab-1-p-1": "אתם יכולים ליצור אנשי קשר וקבוצות, ולנהל שיחות דו-כיווניות, כמו בכל תוכנה אחרת לשליחת הודעות." +} diff --git a/website/langs/it.json b/website/langs/it.json index 66f9c6ff15..fa254e66ef 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -81,7 +81,7 @@ "join": "Unisciti a", "we-invite-you-to-join-the-conversation": "Ti invitiamo a unirti alla conversazione", "enter-your-email-address": "Inserisci il tuo indirizzo email", - "get-simplex": "Ottieni SimpleX", + "get-simplex": "Ottieni SimpleX desktop app", "why-simplex-is": "Perché SimpleX è", "unique": "unico", "learn-more": "Maggiori informazioni", @@ -207,9 +207,9 @@ "simplex-network-1-desc": "Tutti i messaggi vengono inviati tramite i server, garantendo una migliore privacy dei metadati e una consegna asincrona dei messaggi affidabile, evitando molti", "simplex-network-3-desc": "i server forniscono code unidirezionali per connettere gli utenti, ma non hanno visibilità del grafo delle connessioni di rete — solo gli utenti.", "comparison-point-5-text": "Componente centrale o altro attacco a livello di rete", - "comparison-section-list-point-5": "Non protegge i metadati degli utenti", + "comparison-section-list-point-5": "Non protegge la privacy dei metadati degli utenti", "comparison-section-list-point-3": "Chiave pubblica o altro ID univoco globale", - "comparison-section-list-point-4": "Se i server dell'operatore sono compromessi", + "comparison-section-list-point-4": "Se i server dell'operatore sono compromessi. Verifica il codice di sicurezza in Signal e alcune altre app per mitigarlo", "guide-dropdown-1": "Avvio rapido", "guide-dropdown-2": "Inviare messaggi", "guide-dropdown-3": "Gruppi segreti", @@ -232,5 +232,16 @@ "back-to-top": "Torna in cima", "guide-dropdown-8": "Impostazioni dell'app", "docs-dropdown-7": "Traduci SimpleX Chat", - "glossary": "Glossario" + "glossary": "Glossario", + "releases-to-this-repo-are-done-1-2-days-later": "Le pubblicazioni su questo repo avvengono 1-2 giorni dopo", + "f-droid-page-f-droid-org-repo-section-text": "I repository di SimpleX Chat e F-Droid.org firmano i pacchetti con chiavi diverse. Per passare da uno all'altro, esporta il database della chat e reinstalla l'app.", + "signing-key-fingerprint": "Impronta della chiave di firma (SHA-256)", + "f-droid-org-repo": "Repo di F-Droid.org", + "stable-versions-built-by-f-droid-org": "Versioni stabili compilate da F-Droid.org", + "docs-dropdown-8": "Servizio directory di SimpleX", + "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", + "simplex-chat-repo": "Repo di SimpleX Chat", + "stable-and-beta-versions-built-by-developers": "Versioni stabili e beta compilate dagli sviluppatori", + "f-droid-page-simplex-chat-repo-section-text": "Per aggiungerlo al tuo client F-Droid scansiona il codice QR o usa questo URL:", + "comparison-section-list-point-4a": "I relay di SimpleX non possono compromettere la crittografia e2e. Verifica il codice di sicurezza per mitigare gli attacchi sul canale fuori banda" } diff --git a/website/langs/ja.json b/website/langs/ja.json index 9fc7956590..b2ac7ecbc0 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -26,5 +26,222 @@ "simplex-unique-card-1-p-1": "SimpleXは、SimpleXプラットフォームのサーバやその他の観察者から隠すことで、あなたのプロフィール、連絡先やメタデータのプライバシーを守ります。", "simplex-unique-overlay-card-4-p-3": "例えば、SimpleXアプリユーザへのチャットボットやSimpleX Chatライブラリーの携帯アプリへの統合など、SimpleXプラットフォームに関する開発を検討してくださっているようでしたら、どのようなアドバイスや支援のことでもご連絡ください 。", "simplex-unique-overlay-card-4-p-2": "SimpleXプラットフォームは、SimpleX Chatアプリを介してユーザが交流するサービスを実装させつつオープンプロトコルを使い、チャットボットを作成するためにSDKを提供します—私たちはあなた達がどのようなSimpleXのサービスを築くか本当に楽しみです。", - "simplex-unique-overlay-card-4-p-1": "あなたが、自分自身のサーバでSimpleXを使っても、私たちが提供する事前に構築されたサーバを使う方々と連絡を取ることができます。" + "simplex-unique-overlay-card-4-p-1": "あなたが、自分自身のサーバでSimpleXを使っても、私たちが提供する事前に構築されたサーバを使う方々と連絡を取ることができます。", + "reference": "参考文献", + "simplex-explained-tab-1-text": "1. ユーザーが経験すること", + "simplex-explained-tab-1-p-2": "ユーザー プロファイル識別子なしで単方向キューをどのように処理できるのでしょうか?", + "simplex-chat-protocol": "SimpleX チャットプロトコル", + "terminal-cli": "ターミナル CLI", + "terms-and-privacy-policy": "利用規約とプライバシーポリシー", + "hero-header": "プライバシーの基準を新境地に", + "hero-subheader": "
ユーザーIDを持たない最初のメッセンジャー", + "hero-overlay-1-textlink": "ユーザー ID がプライバシーに悪影響を与えるのはなぜですか?", + "hero-overlay-2-textlink": "SimpleXの仕組みは?", + "hero-2-header": "プライベートな接続をする", + "hero-2-header-desc": "このビデオでは、1回限りのQRコード、対面、またはビデオリンクを通じて友人と接続する方法を紹介しています。招待リンクを共有することでも接続できます。", + "simplex-network": "SimpleXネットワーク", + "simplex-explained": "SimpleXの説明", + "simplex-explained-tab-1-p-1": "他のメッセンジャーと同様に、連絡先やグループを作成し、双方向の会話を行うことができます。", + "simplex-explained-tab-2-text": "2. 仕組み", + "simplex-explained-tab-3-text": "3. サーバーが認識するもの", + "smp-protocol": "SMPプロトコル", + "simplex-explained-tab-2-p-1": "接続ごとに 2 つの個別のメッセージング キューを使用して、異なるサーバー経由でメッセージを送受信します。", + "simplex-explained-tab-2-p-2": "サーバーは、ユーザーの会話や接続の全体像を把握することなく、メッセージを一方向に渡すだけです。", + "simplex-explained-tab-3-p-1": "サーバーはキューごとに個別の匿名認証情報を持っており、どのユーザーに属しているかはわかりません。", + "simplex-explained-tab-3-p-2": "ユーザーは、Tor を使用してサーバーにアクセスし、IP アドレスによる相関を防ぐことで、メタデータのプライバシーをさらに向上させることができます。", + "chat-protocol": "チャットプロトコル", + "chat-bot-example": "チャットボットの例", + "donate": "寄付", + "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。
SimpleX にはありません。乱数さえもありません
これにより、プライバシーが大幅に向上します。", + "copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:", + "simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。", + "simplex-private-card-1-point-2": "各キューのNaCL cryptoboxは、TLSが侵害された場合にメッセージキュー間のトラフィック相関を防止します。", + "contact-hero-p-1": "このページを表示したときに、このリンク内の公開キーとメッセージ キュー アドレスはネットワーク経由で送信されません。 それらはリンク URL のハッシュ フラグメントに含まれています。", + "scan-the-qr-code-with-the-simplex-chat-app": "SimpleX Chat アプリで QR コードをスキャン", + "simplex-private-card-9-point-2": "従来のメッセージ ブローカーと比較して、攻撃ベクトルが減少し、利用可能なメタデータが減少します。", + "feature-7-title": "ポータブルな暗号化データベース — プロファイルを別のデバイスに移動する", + "no-federated": "いいえ - 連合型", + "simplex-unique-overlay-card-3-p-3": "電子メール、XMPP、Matrixなどの連携ネットワークサーバーとは異なり、SimpleXサーバーはユーザーアカウントを保存せず、メッセージの中継のみを行い、双方のプライバシーを保護します。", + "privacy-matters-overlay-card-3-p-2": "最も衝撃的な話の 1 つは、Mohamedou Ould Salahiの経験であり、彼の回顧録に記述され、『モーリタニア映画』で紹介されました。 彼は裁判も受けずにグアンタナモ収容所に入れられ、それまで10年間ドイツに住んでいたにも関わらず、9/11攻撃への関与の疑いでアフガニスタンの親戚に電話をかけた後、そこで15年間拷問を受けました。", + "signing-key-fingerprint": "署名キーのフィンガープリント (SHA-256)", + "simplex-network-2-desc": "SimpleX リレー サーバーは、ユーザー プロファイル、連絡先、配信されたメッセージを保存せず、相互に接続せず、サーバー ディレクトリもありません。", + "docs-dropdown-5": "ホストXFTPサーバー", + "simplex-private-card-3-point-2": "サーバーのフィンガープリントとチャネル バインディングにより、MITM 攻撃やリプレイ攻撃を防止します。", + "docs-dropdown-3": "チャットのデータベースへのアクセス", + "installing-simplex-chat-to-terminal": "SimpleX チャットをターミナルにインストールする", + "use-this-command": "次のコマンドを使用してください:", + "to-make-a-connection": "接続するには:", + "comparison-section-list-point-6": "P2P は分散されていますが、統合されておらず、単一のネットワークとして動作します", + "simplex-chat-via-f-droid": "F-Droid 経由の SimpleX チャット", + "privacy-matters-overlay-card-1-p-1": "多くの大企業は、あなたの収入を見積もり、本当に必要のない製品を販売し、価格を決定するために、あなたのつながりに関する情報を使用します。", + "privacy-matters-1-overlay-1-title": "プライバシーの保護はコストを削減します", + "simplex-private-card-5-point-1": "SimpleX は、各暗号化レイヤーにコンテンツ パディングを使用して、メッセージ サイズ攻撃を阻止します。", + "privacy-matters-2-overlay-1-linkText": "プライバシーはあなたに力を与えます", + "enter-your-email-address": "Eメールアドレスを入力してください", + "tap-to-close": "タップして閉じる", + "feature-1-title": "マークダウンと編集を使用可能なE2E 暗号化メッセージ", + "comparison-point-4-text": "単一または集中型ネットワーク", + "guide-dropdown-9": "コネクションを作る", + "simplex-unique-1-overlay-1-title": "ID、プロフィール、連絡先、メタデータの完全なプライバシー", + "hero-overlay-card-2-p-4": "SimpleX は、その設計にユーザー ID を持たないことで、これらの攻撃から保護します。 また、シークレット モードを使用すると、連絡先ごとに異なる表示名が付けられ、連絡先間でデータが共有されることがなくなります。", + "privacy-matters-overlay-card-2-p-2": "客観的であり、独立した意思決定を行うには、情報空間を制御する必要があります。 これは、ソーシャル グラフにアクセスできないプライベート コミュニケーション プラットフォームを使用している場合にのみ可能です。", + "hero-overlay-card-2-p-1": "ユーザーが永続的な ID を持っている場合、それがセッション ID などの単なる乱数であっても、プロバイダーや攻撃者がユーザーの接続方法や送信するメッセージの数を監視できるリスクがあります。", + "feature-3-title": "分散型シークレットグループ —
ユーザーのみがその存在を知っています", + "simplex-network-overlay-1-title": "P2Pメッセージングプロトコルとの比較", + "comparison-section-list-point-7": "P2Pネットワークには中央当局が存在するか、ネットワーク全体が侵害される可能性がある", + "docs-dropdown-1": "SimpleXプラットフォーム", + "hero-overlay-card-1-p-5": "クライアント デバイスのみがユーザー プロファイル、連絡先、およびグループを保存します。 メッセージは 2 レイヤーのエンドツーエンド暗号化を使用して送信されます。", + "simplex-chat-for-the-terminal": "ターミナル用 SimpleX チャット", + "simplex-network-overlay-card-1-li-3": "P2P は MITM 攻撃 問題を解決せず、既存の実装のほとんどは最初の鍵交換に帯域外メッセージを使用していません 。 SimpleX は、最初のキー交換に帯域外メッセージを使用するか、場合によっては既存の安全で信頼できる接続を使用します。", + "the-instructions--source-code": "ソース コードからダウンロードまたはコンパイルする方法を説明します。", + "simplex-network-section-desc": "Simplex Chat は、P2P とフェデレーション ネットワークの利点を組み合わせて最高のプライバシーを提供します。", + "privacy-matters-section-subheader": "メタデータのプライバシーを保護する — 話す相手 — 以下のことからあなたを守ります:", + "if-you-already-installed": "すでにインストールしている場合", + "join": "参加", + "privacy-matters-section-header": "プライバシーが重要である理由", + "on-this-page": "このページでは", + "privacy-matters-overlay-card-1-p-2": "オンライン小売業者は、収入が低い人ほど急ぎの買い物をする可能性が高いことを知っているため、より高い価格を請求したり、割引を廃止したりすることがあります。", + "simplex-unique-3-overlay-1-title": "データの所有権、管理、セキュリティ", + "protocol-3-text": "P2Pプロトコル", + "simplex-private-card-6-point-2": "これを防ぐために、SimpleX アプリは、アドレスをリンクまたは QR コードとして共有するときに、ワンタイム キーを帯域外で渡します。", + "no": "いいえ", + "contact-hero-header": "SimpleX Chatで接続するためのアドレスを受信しました", + "feature-8-title": "シークレット モード —
SimpleX Chat に固有の", + "simplex-private-card-4-point-2": "Tor経由でSimpleXを使用するには、Orbotアプリをインストールし、SOCKS5プロキシを有効にしてください(iOSの場合はVPN)。", + "contact-hero-subheader": "スマホ・タブレットのSimpleX ChatアプリでQRコードを読み取ってください。", + "simplex-unique-2-overlay-1-title": "スパムと悪用からの最高の保護", + "simplex-private-6-title": "帯域外の
鍵交換", + "join-us-on-GitHub": "GitHubで参加する", + "comparison-section-header": "他のプロトコルとの比較", + "invitation-hero-header": "SimpleX Chatで接続するための使い捨てのリンクを受信しました", + "no-secure": "いいえ - 安全", + "hero-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は、他のすべてのプラットフォームで使用されるユーザー ID の代わりに、接続ごとに個別のメッセージ キューの一時的な匿名ペア識別子を使用します — 長期的な識別子はありません。", + "simplex-network-1-header": "P2Pネットワークとは異なります", + "simplex-private-card-7-point-2": "メッセージが追加、削除、または変更されると、受信者に警告が表示されます。", + "simplex-unique-3-title": "データを管理するのはあなたです", + "no-resilient": "いいえ - 弾力性", + "hide-info": "情報を隠す", + "privacy-matters-overlay-card-3-p-4": "エンドツーエンドで暗号化されたメッセンジャーを使用するだけでは十分ではありません。私たちは皆、個人ネットワークのプライバシーを保護するメッセンジャーを使用する必要があります — 私たちがつながっているのは誰なのか。", + "releases-to-this-repo-are-done-1-2-days-later": "このリポジトリへのリリースは 1 ~ 2 日後に行われます", + "comparison-point-1-text": "グローバル ID が必要", + "comparison-section-list-point-5": "ユーザーのメタデータのプライバシーを保護しない", + "hero-overlay-card-2-p-2": "その後、この情報を既存の公開ソーシャル ネットワークと関連付けて、本当の身元を特定することができます。", + "privacy-matters-overlay-card-1-p-3": "一部の金融会社や保険会社は、ソーシャル グラフを使用して金利や保険料を決定しています。 多くの場合、収入の低い人にはより多くの料金を支払わなければなりません。 —これは、「貧困プレミアム」 として知られています。", + "comparison-point-3-text": "DNS への依存", + "yes": "はい", + "docs-dropdown-6": "WebRTC サーバー", + "newer-version-of-eng-msg": "このページには英語版の新しいバージョンがあります。", + "install-simplex-app": "SimpleX アプリをインストールする", + "comparison-point-2-text": "MITMの可能性", + "scan-the-qr-code-with-the-simplex-chat-app-description": "このリンクの公開キーとメッセージ キュー アドレスは、このページを表示するときにネットワーク経由で送信されません。
これらはリンク URL のハッシュ フラグメントに含まれています。", + "open-simplex-app": "SimpleXアプリを開く", + "see-simplex-chat": "SimpleX チャットを見る", + "comparison-section-list-point-1": "通常は電話番号に基づいていますが、場合によってはユーザー名に基づいています", + "github-repository": "GitHub リポジトリ", + "feature-5-title": "消えるメッセージ", + "connect-in-app": "アプリで接続する", + "simplex-private-card-4-point-1": "IP アドレスを保護するために、Tor またはその他のトランスポート オーバーレイ ネットワーク経由でサーバーにアクセスできます。", + "privacy-matters-3-title": "無実の交際による起訴", + "comparison-point-5-text": "中央コンポーネントまたはその他のネットワーク全体の攻撃", + "click-to-see": "クリックして見る", + "donate-here-to-help-us": "寄付はこちらから", + "simplex-private-1-title": "2レイヤーの
エンドツーエンド暗号化", + "privacy-matters-2-overlay-1-title": "プライバシーはあなたに力を与えます", + "simplex-unique-overlay-card-2-p-2": "オプションのユーザー アドレスを使用しても、スパムの連絡先リクエストの送信に使用される可能性がありますが、接続を失うことなく変更または完全に削除できます。", + "simplex-unique-4-overlay-1-title": "完全に分散化されています — ユーザーは SimpleX ネットワークを所有します", + "simplex-network-overlay-card-1-li-5": "すべての既知の P2P ネットワークは、各ノードが検出可能であり、ネットワーク全体が動作するため、Sybil 攻撃に対して脆弱である可能性があります。 この問題を軽減する既知の対策には、一元化されたコンポーネントか、高価な作業証明が必要です。 SimpleX ネットワークにはサーバーの検出機能がなく、断片化されており、複数の分離されたサブネットワークとして動作するため、ネットワーク全体への攻撃は不可能です。", + "simplex-private-2-title": "追加レイヤーの
サーバー暗号化", + "hero-overlay-card-1-p-4": "この設計により、ユーザーの情報の漏洩が防止されます' アプリケーションレベルのメタデータ。 プライバシーをさらに向上させ、IP アドレスを保護するために、Tor 経由でメッセージング サーバーに接続できます。", + "f-droid-org-repo": "F-Droid.org リポジトリ", + "simplex-network-2-header": "連合型ネットワークとは異なります", + "simplex-private-3-title": "セキュアな認証付き
TLSトランスポート", + "comparison-section-list-point-3": "公開キーまたはその他のグローバルに一意な ID", + "hero-overlay-card-2-p-3": "Tor v3 サービスを使用する最もプライベートなアプリであっても、同じプロファイルを介して 2 人の異なる連絡先と会話すると、それらが同じ人物に接続していることが証明される可能性があります。", + "simplex-private-4-title": "オプション
Tor経由のアクセス", + "privacy-matters-1-title": "広告と価格差別", + "hero-overlay-1-title": "SimpleXの仕組みは?", + "stable-versions-built-by-f-droid-org": "F-Droid.org によって構築された安定バージョン", + "contact-hero-p-3": "以下のリンクを使用してアプリをダウンロードしてください。", + "privacy-matters-3-overlay-1-title": "プライバシーはあなたの自由を守ります", + "docs-dropdown-7": "SimpleX チャットを翻訳する", + "simplex-network-1-desc": "すべてのメッセージはサーバー経由で送信され、メタデータのプライバシーが向上し、信頼性の高い非同期メッセージ配信が提供されると同時に、多くが回避されます", + "simplex-chat-repo": "SimpleX チャット リポジトリ", + "simplex-private-card-6-point-1": "多くの通信プラットフォームは、サーバーやネットワーク プロバイダーによる MITM 攻撃に対して脆弱です。", + "privacy-matters-3-overlay-1-linkText": "プライバシーはあなたの自由を守ります", + "simplex-unique-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は一方向メッセージ キューのペアワイズ匿名アドレスを使用し、受信メッセージと送信メッセージに分けて、通常は異なるサーバーを経由します。 SimpleX を使用することは、別の「バーナー」 を使用するようなものです。 連絡先ごとにメールまたは電話を使用できるため、管理に手間がかかりません。", + "simplex-unique-overlay-card-3-p-4": "送受信されるサーバー トラフィックの間に共通の識別子や暗号文はありません。 — 誰かがそれを観察している場合、たとえ TLS が侵害されたとしても、誰が誰と通信しているのかを簡単に判断することはできません。", + "docs-dropdown-2": "Android ファイルへのアクセス", + "get-simplex": "SimpleXを入手する desktop app", + "privacy-matters-overlay-card-3-p-1": "誰もが通信のプライバシーとセキュリティに気を配る必要があります。 たとえ何も隠すものがなかったとしても、無害な会話はあなたを危険にさらす可能性があります。", + "simplex-unique-2-title": "スパムや悪用から
保護されています", + "comparison-section-list-point-2": "DNSベースのアドレス", + "stable-and-beta-versions-built-by-developers": "開発者によって構築された安定版とベータ版", + "simplex-network-3-header": "SimpleX ネットワーク", + "comparison-section-list-point-4": "オペレーターのサーバーが侵害された場合。 Signal およびその他の一部のアプリでセキュリティ コードを検証して緩和する", + "simplex-private-card-2-point-1": "TLSが侵害された場合、受信したサーバー・トラフィックと送信したサーバー・トラフィックの相関を防ぐため、受信者に配信するサーバー暗号化レイヤーを追加します。", + "f-droid-page-simplex-chat-repo-section-text": "F-Droid クライアントに追加するには、QR コードをスキャンするか、次の URL を使用します:", + "join-the-REDDIT-community": "REDDITコミュニティに参加する", + "simplex-private-card-10-point-2": "ユーザー プロファイル識別子なしでメッセージを配信できるため、他の方法よりも優れたメタデータ プライバシーが提供されます。", + "privacy-matters-2-title": "選挙操作", + "simplex-private-card-5-point-2": "これにより、異なるサイズのメッセージがサーバーやネットワーク オブザーバーには同じように見えます。", + "hero-overlay-card-1-p-1": "多くのユーザーは、SimpleX にユーザー識別子がない場合、メッセージの配信先をどのようにして知ることができるのでしょうか? と質問しました", + "feature-6-title": "E2E暗号化された
音声通話とビデオ通話", + "simplex-network-overlay-card-1-li-2": "SimpleX 設計は、ほとんどの P2P ネットワークとは異なり、一時的であってもいかなる種類のグローバル ユーザー識別子も持たず、一時的なペアごとの識別子のみを使用するため、より優れた匿名性とメタデータ保護が提供されます。", + "simplex-unique-4-title": "SimpleX ネットワークを所有", + "privacy-matters-overlay-card-3-p-3": "一般の人が、たとえ「匿名」アカウント経由であっても、オンラインで共有した内容で逮捕されます。たとえ民主主義国家であったとしても。", + "simplex-unique-overlay-card-3-p-2": "エンドツーエンドで暗号化されたメッセージは、SimpleXのリレーサーバーで受信するまで一時的に保持され、その後永久に削除されます。", + "simplex-private-card-7-point-1": "整合性を保証するために、メッセージには連続した番号が付けられ、前のメッセージのハッシュが含まれます。", + "contact-hero-p-2": "SimpleX Chat をまだダウンロードしていませんか?", + "why-simplex-is": "なぜSimpleXなのか", + "simplex-network-section-header": "SimpleX ネットワーク", + "simplex-private-10-title": "一時的な匿名のペア識別子", + "privacy-matters-1-overlay-1-linkText": "プライバシーの保護はコストを削減します", + "tap-the-connect-button-in-the-app": "アプリの 「接続」 ボタンをタップします", + "comparison-section-list-point-4a": "SimpleX リレーは e2e 暗号化を侵害できません。 セキュリティ コードを検証して帯域外チャネルへの攻撃を軽減します", + "unique": "唯一", + "simplex-network-1-overlay-linktext": "P2Pネットワークの問題点", + "no-private": "いいえ - プライベート", + "simplex-unique-1-title": "プライバシーが完全に守られます", + "protocol-2-text": "XMPP、Matrix", + "guide": "ガイド", + "simplex-network-overlay-card-1-li-4": "P2P の実装は、一部のインターネット プロバイダー (BitTorrent など) によってブロックされる場合があります。 SimpleX はトランスポートに依存しません- WebSocketのような標準的な Web プロトコル上で動作します。", + "hero-overlay-2-title": "ユーザー ID がプライバシーに悪影響を与えるのはなぜですか?", + "docs-dropdown-4": "ホストSMPサーバー", + "feature-4-title": "E2E暗号化された音声メッセージ", + "privacy-matters-overlay-card-2-p-1": "つい最近まで、私たちは主要な選挙が 評判の高いコンサルティング会社によって操作されているのを観察しました。 ソーシャルグラフは私たちの現実世界の見方を歪め、私たちの投票を操作します。", + "privacy-matters-overlay-card-2-p-3": "SimpleX は、設計上ユーザー識別子を持たない最初のプラットフォームであり、この方法で既知の代替手段よりも接続グラフを保護します。", + "learn-more": "さらに詳しく", + "simplex-private-8-title": "メッセージのミキシング
相関性を減らす", + "scan-qr-code-from-mobile-app": "モバイルアプリからQRコードをスキャン", + "simplex-private-card-3-point-3": "セッション攻撃を防ぐために、接続の再開は無効になっています。", + "simplex-private-card-10-point-1": "SimpleX は、ユーザー連絡先またはグループ メンバーごとに、一時的な匿名のペアごとのアドレスと資格情報を使用します。", + "more-info": "詳細情報", + "no-decentralized": "いいえ - 分散型", + "protocol-1-text": "Signal、大きなプラットフォーム", + "simplex-network-overlay-card-1-li-6": "P2P ネットワークは、DRDoS 攻撃に対して脆弱になる可能性があります。 クライアントがトラフィックを再ブロードキャストして増幅する可能性があり、その結果、ネットワーク全体のサービス拒否が発生する可能性があります。 SimpleX クライアントは既知の接続からのトラフィックのみを中継するため、攻撃者がネットワーク全体のトラフィックを増幅するために使用することはできません。", + "if-you-already-installed-simplex-chat-for-the-terminal": "すでにターミナルに SimpleX Chat をインストールしている場合", + "docs-dropdown-8": "SimpleX ディレクトリ サービス", + "simplex-private-card-1-point-1": "ダブルラチェットプロトコル —
完全な前方秘匿性と侵入回復機能を備えたOTRメッセージング。", + "simplex-private-card-8-point-1": "SimpleX サーバーは、低遅延の混合ノードとして機能します — 受信メッセージと送信メッセージの順序が異なります。", + "simplex-unique-overlay-card-2-p-1": "SimpleX プラットフォームには識別子がないため、ワンタイムまたは一時的なユーザー アドレスを QR コードまたはリンクとして共有しない限り、誰もあなたに連絡することはできません。", + "sign-up-to-receive-our-updates": "最新情報を受け取る", + "simplex-private-section-header": "SimpleX をプライベートにするもの", + "we-invite-you-to-join-the-conversation": "ぜひ会話にご参加ください", + "feature-2-title": "E2E暗号化された
画像とファイル", + "simplex-private-9-title": "単方向
メッセージキュー", + "simplex-unique-overlay-card-1-p-3": "この設計により、通信相手のプライバシーが保護され、SimpleX プラットフォーム サーバーや監視者からプライバシーが隠されます。 IP アドレスをサーバーから隠すには、Tor 経由で SimpleX サーバーに接続します。", + "simplex-private-7-title": "メッセージの整合性
検証", + "privacy-matters-overlay-card-1-p-4": "SimpleX プラットフォームは、他のどのプラットフォームよりも接続のプライバシーを保護し、ソーシャル グラフが企業や組織に利用されることを完全に防ぎます。 SimpleX Chat が提供するサーバーを使用している場合でも、ユーザーの数や接続数はわかりません。", + "hero-overlay-card-1-p-6": "詳細については、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) 個のノードを順番に通過します。", + "privacy-matters-section-label": "メッセンジャーがあなたのデータにアクセスできないようにしてください!", + "simplex-unique-overlay-card-3-p-1": "SimpleX Chat は、サポートされているデバイスにエクスポートして転送できるポータブル暗号化データベース形式を使用して、すべてのユーザー データをクライアント デバイスにのみ保存します。", + "simplex-network-3-desc": "サーバーはユーザーを接続するための一方向キューを提供しますが、ネットワーク接続グラフは表示されません— ユーザーだけがそうします。", + "simplex-private-card-3-point-1": "クライアント/サーバー接続には、強力なアルゴリズムを備えた TLS 1.2/1.3 のみが使用されます。", + "hero-overlay-card-1-p-3": "メッセージの受信に使用するサーバー、連絡先を定義します —メッセージを送信するために使用するサーバー。 すべての会話では 2 つの異なるサーバーが使用される可能性があります。", + "simplex-unique-overlay-card-1-p-1": "他のメッセージング プラットフォームとは異なり、SimpleX にはユーザーに割り当てられる識別子がありません。 ユーザーを識別するために、電話番号、ドメインベースのアドレス (電子メールや XMPP など)、ユーザー名、公開キー、さらには乱数にも依存しません。 —我々もSimpleX サーバーを何人が使用しているかはわかりません。", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat と F-Droid.org リポジトリは、異なるキーを使用してビルドに署名します。 切り替えるには、チャット データベースをエクスポートし、アプリを再インストールしてください。", + "simplex-private-5-title": "何レイヤーもの
コンテンツパディング" } diff --git a/website/langs/nl.json b/website/langs/nl.json index 897c4cd9a3..02bf471d3b 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -11,7 +11,7 @@ "simplex-explained-tab-1-text": "1. Wat gebruikers ervaren", "simplex-explained-tab-3-text": "3. Wat servers zien", "simplex-explained-tab-1-p-1": "U kunt contacten en groepen maken en tweerichtings gesprekken voeren, zoals in elke andere messenger.", - "simplex-explained-tab-1-p-2": "Hoe kan het werken met unidirectionele wachtrijen en zonder gebruikers profiel ID's?", + "simplex-explained-tab-1-p-2": "Hoe kan het werken met unidirectionele wachtrijen en zonder gebruikers profiel-ID's?", "simplex-explained-tab-3-p-1": "De servers hebben afzonderlijke anonieme inloggegevens voor elke wachtrij en weten niet tot welke gebruikers ze behoren.", "simplex-explained-tab-3-p-2": "Gebruikers kunnen de privacy van metadata verder verbeteren door Tor te gebruiken om toegang te krijgen tot servers, waardoor corellatie op IP-adres wordt voorkomen.", "chat-bot-example": "Chatbot voorbeeld", @@ -21,12 +21,12 @@ "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Voorwaarden & Privacybeleid", - "hero-subheader": "De eerste messenger
zonder gebruikers ID's", - "hero-overlay-1-textlink": "Waarom zijn gebruikers ID's slecht voor de privacy?", + "hero-subheader": "De eerste messenger
zonder gebruikers-ID's", + "hero-overlay-1-textlink": "Waarom zijn gebruikers-ID's slecht voor de privacy?", "hero-overlay-2-textlink": "Hoe werkt SimpleX?", "hero-2-header": "Maak een privé verbinding", "hero-overlay-1-title": "Hoe werkt SimpleX?", - "hero-overlay-2-title": "Waarom zijn gebruikers ID's slecht voor de privacy?", + "hero-overlay-2-title": "Waarom zijn gebruikers-ID's slecht voor de privacy?", "feature-1-title": "E2E-versleutelde berichten met markdown en bewerking", "feature-2-title": "E2E-versleutelde
afbeeldingen en bestanden", "feature-3-title": "Gedecentraliseerde geheime groepen -
alleen gebruikers weten dat ze bestaan", @@ -57,8 +57,8 @@ "chat-protocol": "Chat protocol", "simplex-explained-tab-2-p-1": "Voor elke verbinding gebruikt u twee afzonderlijke berichten wachtrijen om berichten via verschillende servers te verzenden en te ontvangen.", "simplex-explained-tab-2-p-2": "Servers geven berichten slechts in één richting door, zonder een volledig beeld te hebben van het gesprek of de connecties van de gebruiker.", - "hero-p-1": "Andere apps hebben gebruikers ID's: Signal, Matrix, Session, Briar, Jami, Cwtch, enz.
SimpleX niet, zelfs geen willekeurige getallen.
Dit verbetert uw privacy.", - "hero-2-header-desc": "De video laat zien hoe je verbinding maakt met een vriend via een eenmalige QR-code, persoonlijk of via een videolink. U kunt ook verbinding maken door een uitnodigingslink te delen.", + "hero-p-1": "Andere apps hebben gebruikers-ID's: Signal, Matrix, Session, Briar, Jami, Cwtch, enz.
SimpleX niet, zelfs geen willekeurige getallen.
Dit verbetert uw privacy.", + "hero-2-header-desc": "De video laat zien hoe je verbinding maakt met een vriend via een persoonlijk of videolink gedeelde eenmalige QR-code. U kunt ook verbinding maken door een uitnodigingslink te delen.", "hero-header": "Privacy opnieuw gedefinieerd", "feature-7-title": "Portable versleutelde database — verplaats je profiel naar een ander apparaat", "simplex-private-card-1-point-1": "Protocol met double-ratchet -
OTR-berichten met perfecte voorwaartse geheimhouding en inbraak herstel.", @@ -71,7 +71,7 @@ "simplex-private-card-9-point-1": "Elke berichten wachtrij geeft berichten in één richting door, met verschillende verzend en ontvangst adressen.", "simplex-private-card-1-point-2": "NaCL-cryptobox in elke wachtrij om verkeer correlatie tussen berichten wachtrijen te voorkomen als TLS wordt aangetast.", "simplex-private-card-2-point-1": "Extra server versleuteling laag voor levering aan de ontvanger, om de correlatie tussen ontvangen en verzonden server verkeer te voorkomen als TLS is aangetast.", - "simplex-private-card-10-point-2": "Het maakt het mogelijk om berichten te bezorgen zonder gebruikers profiel ID's, wat een betere privacy van metagegevens biedt dan alternatieven.", + "simplex-private-card-10-point-2": "Het maakt het mogelijk om berichten te bezorgen zonder gebruikers profiel-ID's, wat een betere privacy van metagegevens biedt dan alternatieven.", "privacy-matters-1-title": "Reclame en prijs discriminatie", "privacy-matters-1-overlay-1-title": "Privacy bespaart u geld", "privacy-matters-1-overlay-1-linkText": "Privacy bespaart u geld", @@ -95,13 +95,13 @@ "hero-overlay-card-2-p-2": "Ze zouden deze informatie vervolgens kunnen correleren met de bestaande openbare sociale netwerken en een aantal echte identiteiten kunnen bepalen.", "privacy-matters-3-overlay-1-linkText": "Privacy beschermt uw vrijheid", "simplex-unique-2-overlay-1-title": "De beste bescherming tegen spam en misbruik", - "hero-overlay-card-1-p-1": "Veel gebruikers vroegen: als SimpleX geen gebruikers ID's heeft, hoe kan het dan weten waar berichten moeten worden afgeleverd?", - "hero-overlay-card-1-p-2": "Om berichten te bezorgen, gebruikt SimpleX in plaats van gebruikers ID's die door alle andere platforms worden gebruikt, tijdelijke anonieme paarsgewijze identifiers van berichten wachtrijen, gescheiden voor elk van uw verbindingen — er zijn geen identificatiegegevens op lange termijn.", + "hero-overlay-card-1-p-1": "Veel gebruikers vroegen: als SimpleX geen gebruiker-ID's heeft, hoe kan het dan weten waar berichten moeten worden afgeleverd?", + "hero-overlay-card-1-p-2": "Om berichten te bezorgen, gebruikt SimpleX in plaats van gebruikers-ID's die door alle andere platforms worden gebruikt, tijdelijke anonieme paarsgewijze identifiers van berichten wachtrijen, gescheiden voor elk van uw verbindingen — er zijn geen identificatiegegevens op lange termijn.", "hero-overlay-card-2-p-3": "Zelfs met de meest privé apps die Tor v3-services gebruiken, als je met twee verschillende contacten via hetzelfde profiel praat, kunnen ze bewijzen dat ze met dezelfde persoon verbonden zijn.", - "hero-overlay-card-2-p-4": "SimpleX beschermt tegen deze aanvallen door geen gebruikers ID's in het ontwerp te hebben. En als u de incognitomodus gebruikt, heeft u voor elk contact een andere weergavenaam, waardoor gedeelde gegevens tussen hen worden vermeden.", + "hero-overlay-card-2-p-4": "SimpleX beschermt tegen deze aanvallen door geen gebruikers-ID's in het ontwerp te hebben. En als u de incognitomodus gebruikt, heeft u voor elk contact een andere weergavenaam, waardoor gedeelde gegevens tussen hen worden vermeden.", "simplex-network-overlay-card-1-p-1": "P2P-berichtenprotocollen en -apps hebben verschillende problemen waardoor ze minder betrouwbaar zijn dan SimpleX, complexer om te analyseren en kwetsbaarder voor verschillende soorten aanvallen.", "simplex-network-overlay-card-1-li-1": "P2P netwerken vertrouwen op een variant van DHT om berichten te routeren. DHT ontwerpen moeten een balans vinden tussen leveringsgarantie en latentie. SimpleX heeft zowel een betere leveringsgarantie als een lagere latentie dan P2P, omdat het bericht redundant via meerdere servers parallel kan worden doorgegeven, met behulp van de servers die door de ontvanger zijn gekozen. In P2P netwerken wordt het bericht achtereenvolgens door O(log N)-knooppunten geleid, met behulp van door het algoritme gekozen knooppunten.", - "simplex-network-overlay-card-1-li-2": "SimpleX ontwerp heeft, in tegenstelling tot de meeste P2P netwerken, geen globale gebruikers ID's van welke soort dan ook, zelfs niet tijdelijk en gebruikt alleen tijdelijke paarsgewijze ID's wat een betere anonimiteit en metadata bescherming biedt.", + "simplex-network-overlay-card-1-li-2": "SimpleX ontwerp heeft, in tegenstelling tot de meeste P2P netwerken, geen globale gebruikers-ID's van welke soort dan ook, zelfs niet tijdelijk en gebruikt alleen tijdelijke paarsgewijze ID's wat een betere anonimiteit en metadata bescherming biedt.", "simplex-network-overlay-card-1-li-3": "P2P lost het probleem MITM-aanval niet op en de meeste bestaande implementaties gebruiken geen out-of-band berichten voor de initiële sleuteluitwisseling. SimpleX gebruikt out-of-band berichten of, in sommige gevallen, reeds bestaande veilige en vertrouwde verbindingen voor de eerste sleuteluitwisseling.", "simplex-network-overlay-card-1-li-4": "P2P implementaties kunnen door sommige internetproviders worden geblokkeerd (zoals BitTorrent). SimpleX is transport-agnostisch - het kan werken via standaard webprotocollen, b.v. WebSockets.", "simplex-network-overlay-card-1-li-5": "Alle bekende P2P netwerken kunnen kwetsbaar zijn voor Sybil-aanval, omdat elk knooppunt vindbaar is en het netwerk als een geheel opereert. Bekende maatregelen om het te verminderen, vereisen een gecentraliseerde component of duur bewijs van werk . Het SimpleX-netwerk kan niet worden gedetecteerd door de server, het is gefragmenteerd en werkt als meerdere geïsoleerde subnetwerken, waardoor netwerkbrede aanvallen onmogelijk zijn.", @@ -177,7 +177,7 @@ "privacy-matters-overlay-card-1-p-3": "Sommige financiële en verzekeringsmaatschappijen gebruiken sociale grafieken om rentetarieven en premies te bepalen. Het zorgt er vaak voor dat mensen met lagere inkomens meer betalen — het staat bekend als 'armoedepremie'.", "privacy-matters-overlay-card-1-p-4": "Het SimpleX platform beschermt de privacy van uw verbindingen beter dan welk alternatief dan ook, waardoor volledig wordt voorkomen dat uw sociale grafiek beschikbaar wordt voor bedrijven of organisaties. Zelfs wanneer mensen servers van SimpleX Chat gebruiken, weten we niet hoeveel gebruikers of hun verbindingen zijn.", "privacy-matters-overlay-card-2-p-2": "Om objectief te zijn en onafhankelijke beslissingen te kunnen nemen, moet u de controle hebben over uw informatie. Het is alleen mogelijk als u een privé communicatieplatform gebruikt dat geen toegang heeft tot uw sociale grafiek.", - "privacy-matters-overlay-card-2-p-3": "SimpleX is het eerste platform dat standaard geen gebruikers ID's heeft, waardoor uw verbindingsgrafiek beter wordt beschermd dan welk alternatief dan ook.", + "privacy-matters-overlay-card-2-p-3": "SimpleX is het eerste platform dat standaard geen gebruikers-ID's heeft, waardoor uw verbindingsgrafiek beter wordt beschermd dan welk alternatief dan ook.", "privacy-matters-overlay-card-3-p-1": "Iedereen zou zich zorgen moeten maken over de privacy en veiligheid van hun communicatie — onschuldige gesprekken kunnen je in gevaar brengen, zelfs als je niets te verbergen hebt.", "simplex-unique-overlay-card-1-p-1": "In tegenstelling tot andere berichten platforms heeft SimpleX geen identifiers toegewezen aan de gebruikers . Het vertrouwt niet op telefoonnummers, domein gebaseerde adressen (zoals e-mail of XMPP), gebruikersnamen, openbare sleutels of zelfs willekeurige nummers om zijn gebruikers te identificeren — we weten niet ' hoeveel mensen onze SimpleX servers gebruiken.", "privacy-matters-overlay-card-3-p-2": "Een van de meest schokkende verhalen is de ervaring van Mohamedou Ould Salahi beschreven in zijn memoires en getoond in The Mauritanian movie. Hij werd zonder proces in het Guantanamo kamp geplaatst en werd daar 15 jaar lang gemarteld na een telefoontje naar zijn familielid in Afghanistan, op verdenking van betrokkenheid bij aanslagen van 9/11, ook al woonde hij de afgelopen 10 jaar in Duitsland.", @@ -194,7 +194,7 @@ "simplex-unique-overlay-card-4-p-3": "Als u overweegt om voor het SimpleX platform te ontwikkelen, bijvoorbeeld de chatbot voor gebruikers van de SimpleX app, of de integratie van de SimpleX Chat bibliotheek in uw mobiele apps, get in touch for any advice en ondersteuning.", "simplex-unique-card-1-p-1": "SimpleX beschermt de privacy van uw profiel, contacten en metadata en verbergt deze voor SimpleX platformservers en eventuele waarnemers.", "enter-your-email-address": "Voer uw e-mail adres in", - "get-simplex": "Verkrijg SimpleX", + "get-simplex": "Verkrijg SimpleX desktop app", "tap-to-close": "Tik om te sluiten", "contact-hero-header": "Je hebt een adres ontvangen om verbinding te maken met SimpleX Chat", "invitation-hero-header": "Je hebt een eenmalige link ontvangen om verbinding te maken met SimpleX Chat", @@ -204,8 +204,8 @@ "simplex-unique-card-1-p-2": "In tegenstelling tot elk ander bestaand berichten platform, heeft SimpleX geen identificatiecodes toegewezen aan de gebruikers — zelfs geen willekeurige nummers .", "comparison-section-list-point-1": "Meestal op basis van een telefoonnummer, in sommige gevallen op gebruikersnamen", "comparison-section-list-point-2": "Op DNS gebaseerde adressen", - "comparison-section-list-point-4": "Als de servers van de operator zijn aangetast", - "comparison-section-list-point-5": "Beschermt de metadata van gebruikers niet", + "comparison-section-list-point-4": "Als de servers van de operator zijn gecompromitteerd. Controleer de beveiligingscode in Signal en enkele andere apps om dit te beperken", + "comparison-section-list-point-5": "Beschermt de privacy van metagegevens van gebruikers niet", "comparison-section-list-point-6": "Hoewel P2P wordt gedistribueerd, zijn ze niet federatief - ze werken als een enkel netwerk", "see-here": "kijk hier", "comparison-section-list-point-7": "P2P netwerken hebben een centrale autoriteit of het hele netwerk kan worden aangetast", @@ -232,5 +232,16 @@ "click-to-see": "Klik om te zien", "back-to-top": "Terug naar boven", "newer-version-of-eng-msg": "Er is een nieuwere versie van deze pagina in het Engels.", - "glossary": "Woordenlijst" + "glossary": "Woordenlijst", + "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", + "simplex-chat-repo": "SimpleX Chat repo", + "stable-and-beta-versions-built-by-developers": "Stabiele en bètaversies gebouwd door de ontwikkelaars", + "f-droid-page-simplex-chat-repo-section-text": "Om het toe te voegen aan uw F-Droid-client scan de QR-code of gebruik deze URL:", + "f-droid-org-repo": "F-Droid.org repo", + "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", + "stable-versions-built-by-f-droid-org": "Stabiele versies gebouwd door F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden 1-2 dagen later plaats", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- en F-Droid.org-repository's ondertekenen builds met de verschillende sleutels. Om over te stappen, alstublieft exporteer de chatdatabase en installeer de app opnieuw.", + "docs-dropdown-8": "SimpleX Directory Service", + "comparison-section-list-point-4a": "SimpleX relais kunnen de e2e-versleuteling niet in gevaar brengen. Controleer de beveiligingscode om aanvallen op out-of-band kanalen te beperken" } diff --git a/website/langs/pl.json b/website/langs/pl.json index 3c3dd26e1d..f9b9936848 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -98,7 +98,7 @@ "no-federated": "Nie - sfederowany", "comparison-section-list-point-1": "Zazwyczaj na podstawie numeru telefonu, w niektórych przypadkach na podstawie nazwy użytkownika", "comparison-section-list-point-2": "Adresy oparte na DNS", - "comparison-section-list-point-5": "Nie chroni metadanych użytkowników", + "comparison-section-list-point-5": "Nie chroni prywatności metadanych użytkowników", "see-here": "zobacz tutaj", "simplex-unique-1-title": "Masz pełną prywatność", "hero-overlay-card-1-p-3": "Ty określasz, którego serwera (serwerów) użyć do odbierania wiadomości, Twoje kontakty — serwery, których używasz do wysyłania do nich wiadomości. Każda rozmowa prawdopodobnie będzie korzystać z dwóch różnych serwerów.", @@ -159,7 +159,7 @@ "simplex-unique-card-4-p-2": "Możesz używać SimpleX z własnymi serwerami lub z serwerami dostarczonymi przez nas — i nadal łączyć się z dowolnym użytkownikiem.", "we-invite-you-to-join-the-conversation": "Zapraszamy do udziału w rozmowie", "enter-your-email-address": "Wpisz swój adres e-mail", - "get-simplex": "Pobierz SimpleX", + "get-simplex": "Pobierz SimpleX desktop app", "why-simplex-is": "Dlaczego SimpleX jest", "join": "Dołącz do", "join-us-on-GitHub": "Dołącz do nas na GitHubie", @@ -208,7 +208,7 @@ "protocol-2-text": "XMPP, Matrix", "protocol-3-text": "Protokoły sieci P2P", "comparison-section-list-point-6": "Podczas gdy sieci P2P są rozproszone, nie są sfederowane - działają jako jedna sieć", - "comparison-section-list-point-4": "Jeśli bezpieczeństwo serwerów operatora zostało naruszone", + "comparison-section-list-point-4": "Jeśli bezpieczeństwo serwerów operatora zostało naruszone. Zweryfikuj kody bezpieczeństwa w Signalu lub innej aplikacji aby to złagodzić", "comparison-section-list-point-7": "Sieci P2P albo mają centralny organ, albo cała sieć może zostać skompromitowana", "guide-dropdown-1": "Szybki start", "guide-dropdown-2": "Wysyłanie wiadomości", @@ -232,5 +232,16 @@ "menu": "Menu", "on-this-page": "Na tej stronie", "back-to-top": "Powrót do góry", - "glossary": "Słowniczek" + "glossary": "Słowniczek", + "f-droid-page-simplex-chat-repo-section-text": "Aby dodać do Twojego klienta F-Droid, zeskanuj kod QR lub użyj tego URL:", + "f-droid-page-f-droid-org-repo-section-text": "Repozytoria SimpleX Chat i F-Droid.org mają podpisane budowy z innymi kluczami. Aby zmienić, proszę wyeksportuj bazę czatu i przeinstaluj aplikację.", + "docs-dropdown-8": "Serwis katalogowy SimpleX", + "simplex-chat-via-f-droid": "SimpleX Chat na F-Droid", + "simplex-chat-repo": "Repo SimpleX", + "stable-and-beta-versions-built-by-developers": "Wersje stabilne i beta zbudowane przez deweloperów", + "signing-key-fingerprint": "Odcisk klucza podpisu (SHA-256)", + "f-droid-org-repo": "Repo F-Droid.org", + "stable-versions-built-by-f-droid-org": "Wersje stabilne zbudowane przez F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "Wydania na tym repo są 1-2 dni później", + "comparison-section-list-point-4a": "Przekaźniki SimpleX nie mogą skompromitować szyfrowania e2e. Zweryfikuj kody bezpieczeństwa aby złagodzić atak na kanał pozapasmowy" } diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 083f4942ec..278d54c43a 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -146,7 +146,7 @@ "donate-here-to-help-us": "Doe aqui para nos ajudar", "sign-up-to-receive-our-updates": "Inscreva-se para receber nossas atualizações", "enter-your-email-address": "Digite seu endereço de e-mail", - "get-simplex": "Obtenha o SimpleX", + "get-simplex": "Obtenha o SimpleX desktop app", "why-simplex-is": "Por que o SimpleX é", "unique": "único", "learn-more": "Saiba mais", diff --git a/website/langs/uk.json b/website/langs/uk.json index 22bf811c22..6a48cc5640 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -49,7 +49,7 @@ "docs-dropdown-1": "Платформа SimpleX", "docs-dropdown-2": "Доступ до файлів Android", "docs-dropdown-3": "Доступ до бази даних чату", - "docs-dropdown-4": "Сервер SMP на хості", + "docs-dropdown-4": "Хост SMP-сервер", "docs-dropdown-5": "Хост XFTP-сервер", "docs-dropdown-6": "Сервери WebRTC", "docs-dropdown-7": "Перекласти чат SimpleX", @@ -107,7 +107,7 @@ "simplex-private-9-title": "Односпрямовані
черги повідомлень", "simplex-private-8-title": "Змішування повідомлень
для зменшення кореляції", "simplex-private-10-title": "Тимчасові анонімні парні ідентифікатори", - "simplex-private-card-1-point-1": "Протокол з подвійним храповиком —
ОТР-обмін повідомленнями з ідеальною прямою секретністю та можливістю відновлення після злому.", + "simplex-private-card-1-point-1": "Протокол double-ratchet —
OTR messaging з ідеальною конфіденційністю та відновленням зламів.", "simplex-private-card-1-point-2": "Криптобокс NaCL у кожній черзі для запобігання кореляції трафіку між чергами повідомлень, якщо TLS скомпрометовано.", "simplex-private-card-2-point-1": "Додатковий рівень серверного шифрування для доставки одержувачу, щоб запобігти кореляції між отриманим і відправленим серверним трафіком, якщо TLS скомпрометовано.", "simplex-private-card-3-point-1": "Для клієнт-серверних з'єднань використовується тільки TLS 1.2/1.3 з сильними алгоритмами.", @@ -117,9 +117,9 @@ "simplex-private-card-5-point-1": "SimpleX використовує заповнення вмісту для кожного рівня шифрування, щоб запобігти атакам на розмір повідомлення.", "simplex-private-card-5-point-2": "Це дозволяє повідомленням різного розміру виглядати однаково для серверів і мережевих спостерігачів.", "simplex-private-card-6-point-1": "Багато комунікаційних платформ вразливі до MITM-атак з боку серверів або мережевих провайдерів.", - "simplex-private-card-6-point-2": "Щоб запобігти цьому, додатки SimpleX передають одноразові ключі поза смугою пропускання, коли ви ділитеся адресою у вигляді посилання або QR-коду.", + "simplex-private-card-6-point-2": "Щоб запобігти цьому, програми SimpleX передають одноразові ключі поза смугою, коли ви надаєте адресу як посилання або QR-код.", "simplex-private-card-7-point-2": "Якщо будь-яке повідомлення буде додано, видалено або змінено, одержувач отримає сповіщення.", - "simplex-private-card-8-point-1": "Сервери SimpleX працюють як змішані вузли з низькою затримкою — вхідні та вихідні повідомлення мають різний порядок.", + "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 використовує тимчасові анонімні попарні адреси та облікові дані для кожного контакту користувача або члена групи.", @@ -140,7 +140,7 @@ "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-2": "Для доставки повідомлень замість ідентифікаторів користувачів, які використовуються всіма іншими платформами, SimpleX використовує тимчасові анонімні парні ідентифікатори черг повідомлень, окремі для кожного з ваших з'єднань — немає ніяких довгострокових ідентифікаторів.", "hero-overlay-card-1-p-3": "Ви визначаєте, який сервер(и) використовувати для отримання повідомлень, ваші контакти — сервери, які ви використовуєте для надсилання їм повідомлень. У кожній розмові, швидше за все, будуть використовуватися два різних сервери.", "hero-overlay-card-1-p-4": "Такий дизайн запобігає витоку будь-яких метаданих користувачів на рівні програми. Щоб ще більше підвищити конфіденційність і захистити свою IP-адресу, ви можете підключитися до серверів обміну повідомленнями через Tor.", "hero-overlay-card-1-p-6": "Читайте більше в Пропонуємо ознайомитися з технічним документом SimpleX.", @@ -149,7 +149,7 @@ "hero-overlay-card-2-p-4": "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-2": "Дизайн SimpleX, на відміну від більшості мереж P2P, не має жодних глобальних ідентифікаторів користувачів, навіть тимчасових, а використовує тимчасові ідентифікатори лише парами, забезпечуючи кращу анонімність і захист метаданих.", "privacy-matters-overlay-card-1-p-1": "Багато великих компаній використовують інформацію про те, з ким ви пов'язані, щоб оцінити ваш дохід, продати вам товари, які вам насправді не потрібні, і визначити ціни.", "privacy-matters-overlay-card-3-p-3": "Звичайних людей заарештовують за те, чим вони діляться в Інтернеті, навіть через свої 'анонімні' облікові записи, навіть у демократичних країнах.", "simplex-unique-overlay-card-1-p-2": "Для доставки повідомлень SimpleX використовує парні анонімні адреси односпрямованих черг повідомлень, окремо для отриманих і відправлених повідомлень, зазвичай через різні сервери. Використання SimpleX - це все одно, що мати різну “конфорку” електронну пошту або телефон для кожного контакту, і не мати клопоту з управлінням ними.", @@ -173,7 +173,7 @@ "donate-here-to-help-us": "Пожертвуйте тут, щоб допомогти нам", "sign-up-to-receive-our-updates": "Підпишіться на наші оновлення", "enter-your-email-address": "Введіть адресу вашої електронної пошти", - "get-simplex": "Отримати SimpleX", + "get-simplex": "Отримати SimpleX desktop app", "why-simplex-is": "Чому SimpleX це", "unique": "унікальний", "learn-more": "Дізнайтеся більше", @@ -232,5 +232,15 @@ "guide-dropdown-1": "Швидкий старт", "guide-dropdown-2": "Надсилання повідомлень", "guide-dropdown-3": "Таємні групи", - "glossary": "Глосарій" -} + "glossary": "Глосарій", + "docs-dropdown-8": "Служба каталогів SimpleX", + "f-droid-page-simplex-chat-repo-section-text": "Щоб додати його в клієнт F-Droid відскануйте QR-код або використовуйте цю URL-адресу:", + "simplex-chat-via-f-droid": "SimpleX Chat через F-Droid", + "signing-key-fingerprint": "Відбиток ключа підпису (SHA-256)", + "stable-versions-built-by-f-droid-org": "Стабільні версії, зібрані на F-Droid.org", + "simplex-chat-repo": "Репо SimpleX Chat", + "f-droid-org-repo": "Репо F-Droid.org", + "releases-to-this-repo-are-done-1-2-days-later": "Релізи в це репо відбуваються на 1-2 дні пізніше", + "stable-and-beta-versions-built-by-developers": "Стабільні та бета-версії, створені розробниками", + "f-droid-page-f-droid-org-repo-section-text": "Репозиторії SimpleX Chat та F-Droid.org підписують збірки з різними ключами. Щоб перемикнутися, будь ласка експорт базу даних чату та перевстановіть додаток." +} \ No newline at end of file diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 4a1bbcf2d3..c327e160be 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -26,7 +26,7 @@ "simplex-unique-overlay-card-3-p-4": "发送和接收的服务器流量之间没有共同的标识符或密文—— 如果有人在观察它,他们也无法轻易确定谁与谁通信,即使 TLS 受到威胁。", "simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他平台。", "join": "加入", - "get-simplex": "获取 SimpleX", + "get-simplex": "获取 SimpleX desktop app", "hide-info": "隐藏信息", "contact-hero-header": "您收到了一个用于连接 SimpleX Chat 的地址", "contact-hero-p-2": "还没有下载 SimpleX Chat 吗?", @@ -194,7 +194,7 @@ "no-resilient": "不需要 - 有抗御力", "no-decentralized": "不需要 - 去中心化的", "comparison-section-list-point-3": "公钥或其他一些全球唯一的 ID", - "comparison-section-list-point-4": "如果运营商的服务器受到威胁", + "comparison-section-list-point-4": "如果运营商的服务器受到威胁。 验证 Signal 和其他一些应用程序中的安全代码以缓解该问题", "comparison-section-list-point-1": "通常基于电话号码,在某些情况下基于用户名", "comparison-section-list-point-2": "基于 DNS 的地址", "comparison-section-list-point-6": "P2P 是分布式的,而非联邦式的 - 它们作为单个网络运行", @@ -232,5 +232,16 @@ "guide-dropdown-9": "建立连接", "back-to-top": "回到页首", "on-this-page": "在此页面上", - "glossary": "术语表" + "glossary": "术语表", + "signing-key-fingerprint": "签名密钥指纹 (SHA-256)", + "simplex-chat-via-f-droid": "通过 F-Droid 下载 SimpleX", + "releases-to-this-repo-are-done-1-2-days-later": "此存储库的版本将延迟 1-2 天发布", + "f-droid-org-repo": "F-Droid.org 存储库", + "stable-versions-built-by-f-droid-org": "由 F-Droid.org 构建的稳定版本", + "simplex-chat-repo": "SimpleX 存储库", + "stable-and-beta-versions-built-by-developers": "开发人员构建的稳定版和测试版", + "f-droid-page-simplex-chat-repo-section-text": "要将其添加到您的 F-Droid 客户端,请扫描二维码或使用以下 URL:", + "comparison-section-list-point-4a": "SimpleX 中继无法破坏 e2e 加密。 验证安全代码以减轻对带外通道的攻击", + "docs-dropdown-8": "SimpleX 目录服务", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat 和 F-Droid.org 存储库使用不同的密钥对构建进行签名。 如需切换,请导出聊天数据库并重新安装应用。" } diff --git a/website/package.json b/website/package.json index 5eddf63031..bbe3a17a68 100644 --- a/website/package.json +++ b/website/package.json @@ -32,6 +32,7 @@ "fs": "^0.0.1-security", "gray-matter": "^4.0.3", "jsdom": "^22.1.0", + "lottie-web": "5.12.2", "markdown-it": "^13.0.1" } } diff --git a/website/src/.well-known/README.md b/website/src/.well-known/README.md index ec4c7f57e8..6346c85a76 100644 --- a/website/src/.well-known/README.md +++ b/website/src/.well-known/README.md @@ -12,4 +12,6 @@ File `assetlinks.json` includes certificate hashes for: ## iOS -`apple-app-site-association` currently does not work, as it needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension. +`apple-app-site-association` needs to be served with `Content-type: application/json; charset=utf-8` and GitHub pages do not support adding this header to files without JSON extension. + +To workaround this (thanks to [StackOverflow - Serve json data from github pages](https://stackoverflow.com/questions/39199042/serve-json-data-from-github-pages)) we're creating directory named `apple-app-site-association` with `index.json` file that contains all the necessary configs. \ No newline at end of file diff --git a/website/src/.well-known/apple-app-site-association b/website/src/.well-known/apple-app-site-association/index.json similarity index 89% rename from website/src/.well-known/apple-app-site-association rename to website/src/.well-known/apple-app-site-association/index.json index 956e006f25..3cd3fdd043 100644 --- a/website/src/.well-known/apple-app-site-association +++ b/website/src/.well-known/apple-app-site-association/index.json @@ -3,7 +3,7 @@ "details": [ { "appIDs": [ - "5NN7GUYB6.chat.simplex.app" + "5NN7GUYB6T.chat.simplex.app" ], "components": [ { diff --git a/website/src/.well-known/nostr.json b/website/src/.well-known/nostr.json new file mode 100644 index 0000000000..8c409fb86c --- /dev/null +++ b/website/src/.well-known/nostr.json @@ -0,0 +1,18 @@ +{ + "names": { + "_": "c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171" + }, + "relays": { + "c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171": [ + "wss://nostr.orangepill.dev", + "wss://eden.nostr.land", + "wss://relay.damus.io", + "wss://relay.snort.social", + "wss://relay.current.fyi", + "wss://nos.lol", + "wss://relay.nostr.bg", + "wss://nostr-verified.wellorder.net", + "wss://nostr.milou.lol" + ] + } +} diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index ff05242c81..a8c6e634b2 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -4,6 +4,10 @@ "title": "docs-dropdown-1", "url": "/docs/simplex.html" }, + { + "title": "docs-dropdown-8", + "url": "/docs/directory.html" + }, { "title": "docs-dropdown-2", "url": "/docs/android.html" @@ -27,6 +31,10 @@ { "title": "docs-dropdown-7", "url": "/docs/translations.html" + }, + { + "title": "docs-dropdown-9", + "url": "/downloads/" } ] } \ No newline at end of file diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index f05ae0ab1a..fcd980121d 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -18,6 +18,7 @@ "menu": "Reference", "data": [ "SIMPLEX.md", + "DIRECTORY.md", "ANDROID.md", "CLI.md", "SQL.md", @@ -25,7 +26,8 @@ "SERVER.md", "TRANSLATIONS.md", "WEBRTC.md", - "XFTP-SERVER.md" + "XFTP-SERVER.md", + "DOWNLOADS.md" ] }, { diff --git a/website/src/_data/hero_overlays.json b/website/src/_data/hero_overlays.json index d50ba0db68..7388994459 100644 --- a/website/src/_data/hero_overlays.json +++ b/website/src/_data/hero_overlays.json @@ -23,6 +23,18 @@ "showImage": true, "contentBody": "overlay_content/hero/card_2.html" } + }, + { + "id": 3, + "imgLight": "/img/trail-of-bits-light.png", + "imgDark": "/img/trail-of-bits-dark.png", + "overlayContent": { + "overlayId": "security-assessment", + "overlayScrollTo": "", + "title": "hero-overlay-3-title", + "showImage": true, + "contentBody": "overlay_content/hero/card_3.html" + } } ] } \ No newline at end of file diff --git a/website/src/_data/languages.json b/website/src/_data/languages.json index 3629172642..0ac05063cc 100644 --- a/website/src/_data/languages.json +++ b/website/src/_data/languages.json @@ -45,6 +45,12 @@ "flag": "/img/flags/it.svg", "enabled": true }, + { + "label": "ja", + "name": "日本語", + "flag": "/img/flags/jp.svg", + "enabled": true + }, { "label": "nl", "name": "Nederlands", diff --git a/website/src/_includes/blog_previews/20230925.html b/website/src/_includes/blog_previews/20230925.html new file mode 100644 index 0000000000..9fb062639e --- /dev/null +++ b/website/src/_includes/blog_previews/20230925.html @@ -0,0 +1,14 @@ +

v5.3 is released:

+ +
    +
  • new desktop app! 💻
  • +
  • encrypt locally storead files & media
  • +
  • directory service and other group improvements
  • +
  • simplified incognito mode
  • +
  • better app responsiveness and stability, and 40% reduced memory usage.
  • +
  • new privacy settings: show last messages & save draft.
  • +
+ +

Also, our users added 6 new interface languages - Arabic*, Bulgarian, Finnish, Hebrew*, Thai and Ukrainian.

+ +

* Android app only.

diff --git a/website/src/_includes/contact_page.html b/website/src/_includes/contact_page.html index e37dcddd0d..a03a3aea4f 100644 --- a/website/src/_includes/contact_page.html +++ b/website/src/_includes/contact_page.html @@ -1,3 +1,4 @@ +{% set lang = page.url | getlang %} {% block js_scripts %} @@ -18,7 +19,7 @@
- +
@@ -51,7 +52,7 @@
- +
diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index 38001afd96..c0e2b3f30b 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -1,3 +1,4 @@ +{% set lang = page.url | getlang %}
@@ -16,6 +17,8 @@ {{ overlay(hero_overlays.sections[1], lang) }} {{ "hero-overlay-2-textlink" | i18n({}, lang ) | safe }} {{ overlay(hero_overlays.sections[0], lang) }} + {{ "hero-overlay-3-textlink" | i18n({}, lang ) | safe }} + {{ overlay(hero_overlays.sections[2], lang) }}
@@ -46,7 +49,7 @@
- +
diff --git a/website/src/_includes/layouts/jobs.html b/website/src/_includes/layouts/jobs.html new file mode 100644 index 0000000000..6a68c0795f --- /dev/null +++ b/website/src/_includes/layouts/jobs.html @@ -0,0 +1,45 @@ + + + + + + + + {{ title }} + + + + + + + + + + + +
+ {% include "navbar.html" %} +
+ +
+
+
{{ content | wrapH3s(page) | safe }}
+
+
+ + {% include "footer.html" %} + + + + \ No newline at end of file diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index beb3139c2b..55836bd576 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -100,6 +100,14 @@
+ + +
+
- {% if 'blog' not in page.url %} + {% if ('blog' not in page.url) and ('jobs' not in page.url) %}